From c55cc76def5b486d01aba88a5900191cd6cc20b4 Mon Sep 17 00:00:00 2001 From: Bolton Bailey Date: Sun, 30 Aug 2026 07:33:00 -0700 Subject: [PATCH] feat(PCP): the PCP theorem and the material needed to prove it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `PCP_theorem`: `NP` equals the union of `PCP r q` over constructible `r =O log` and constant `q`, proved via Dinur's gap amplification (the `NP ⊆ PCP` direction) and a guess-and-verify simulation (`PCP ⊆ NP`). Cut down to what the proof actually uses, at declaration level: the `Classes/PCP` tree (Defs, surface, 115 internal modules, with ~150 declarations unreachable from `PCP_theorem` removed), and a small `FP` toolkit relocated to `Classes/P/Cobham/Internal` (pair-decoder bridge, head-of-list ops, polynomial rulers, bounded binary values and counter increments, and a P language as an FP decision function). Adds `PolyBound.exists_mul_pow_bound` and `pairSnd_length_le` to their home files. Co-Authored-By: Claude Fable 5 --- Complexitylib/Asymptotics/PolyBound.lean | 16 + Complexitylib/Classes.lean | 1 + Complexitylib/Classes/P/Cobham.lean | 4 + Complexitylib/Classes/P/Cobham/Internal.lean | 2 + .../Classes/P/Cobham/Internal/BinValLE.lean | 129 ++ .../Classes/P/Cobham/Internal/BumpBits.lean | 300 +++++ .../Classes/P/Cobham/Internal/CodeAccept.lean | 91 ++ .../Classes/P/Cobham/Internal/CodeRewind.lean | 141 ++ .../Classes/P/Cobham/Internal/CodeStep.lean | 66 + .../Classes/P/Cobham/Internal/FPBridge.lean | 141 ++ .../Classes/P/Cobham/Internal/HeadOps.lean | 52 + .../Classes/P/Cobham/Internal/Iterate.lean | 90 +- .../Classes/P/Cobham/Internal/PVerdict.lean | 373 ++++++ .../Classes/P/Cobham/Internal/PolyLen.lean | 101 ++ .../Classes/P/Cobham/Internal/PolyRuler.lean | 73 ++ .../Classes/P/Cobham/Internal/StringOps.lean | 328 +++++ Complexitylib/Classes/P/DecisionFn.lean | 52 + Complexitylib/Classes/PCP.lean | 47 + Complexitylib/Classes/PCP/Defs.lean | 188 +++ Complexitylib/Classes/PCP/Internal.lean | 128 ++ .../Classes/PCP/Internal/AlgCSPModel.lean | 319 +++++ .../Classes/PCP/Internal/AlgCompose.lean | 78 ++ .../Classes/PCP/Internal/AlgEdge.lean | 433 ++++++ .../Classes/PCP/Internal/AlgFamily.lean | 255 ++++ .../Classes/PCP/Internal/AlgFormula.lean | 94 ++ .../Classes/PCP/Internal/AlgGap.lean | 164 +++ .../Classes/PCP/Internal/AlgGapAll.lean | 179 +++ .../Classes/PCP/Internal/AlgGapCSP.lean | 197 +++ .../Classes/PCP/Internal/AlgGraph.lean | 211 +++ .../Classes/PCP/Internal/AlgInit.lean | 254 ++++ .../Classes/PCP/Internal/AlgIter.lean | 59 + .../Classes/PCP/Internal/AlgKey.lean | 244 ++++ .../Classes/PCP/Internal/AlgKeyFn.lean | 673 ++++++++++ .../Classes/PCP/Internal/AlgKilled.lean | 543 ++++++++ .../Classes/PCP/Internal/AlgLevel.lean | 318 +++++ .../Classes/PCP/Internal/AlgLog.lean | 235 ++++ .../Classes/PCP/Internal/AlgPCP.lean | 209 +++ .../Classes/PCP/Internal/AlgPosNum.lean | 149 +++ .../Classes/PCP/Internal/AlgPreRel.lean | 171 +++ .../Classes/PCP/Internal/AlgPreRot.lean | 789 +++++++++++ .../Classes/PCP/Internal/AlgPreprocess.lean | 256 ++++ .../Classes/PCP/Internal/AlgRound.lean | 174 +++ .../Classes/PCP/Internal/AlgStep.lean | 251 ++++ .../Classes/PCP/Internal/AlgUniform.lean | 118 ++ .../Classes/PCP/Internal/AlgWalk.lean | 208 +++ .../Classes/PCP/Internal/AlphabetLift.lean | 99 ++ .../Classes/PCP/Internal/Amplification.lean | 167 +++ .../Classes/PCP/Internal/Arithmetize.lean | 342 +++++ .../Classes/PCP/Internal/BaseAlg.lean | 390 ++++++ .../Classes/PCP/Internal/BinToUnary.lean | 120 ++ .../Classes/PCP/Internal/BitwiseFP.lean | 105 ++ .../Classes/PCP/Internal/BoundedQuant.lean | 72 + .../Classes/PCP/Internal/CNFCount.lean | 195 +++ .../Classes/PCP/Internal/CNFMaxVar.lean | 160 +++ .../Classes/PCP/Internal/CNFSegment.lean | 398 ++++++ .../Classes/PCP/Internal/CNFTokens.lean | 319 +++++ .../Classes/PCP/Internal/CSPVerifier.lean | 513 ++++++++ .../Classes/PCP/Internal/Cheeger.lean | 565 ++++++++ .../Classes/PCP/Internal/CloudCount.lean | 109 ++ .../PCP/Internal/CloudDisagreement.lean | 264 ++++ .../Classes/PCP/Internal/CoinEnum.lean | 141 ++ .../Classes/PCP/Internal/Compose.lean | 415 ++++++ .../Classes/PCP/Internal/ConsistencyLang.lean | 476 +++++++ .../Classes/PCP/Internal/ConstraintGraph.lean | 223 ++++ .../Classes/PCP/Internal/CubeBlocks.lean | 323 +++++ .../Classes/PCP/Internal/DartCorrelation.lean | 538 ++++++++ .../Classes/PCP/Internal/DataScan.lean | 351 +++++ .../Classes/PCP/Internal/DataScanSpec.lean | 207 +++ .../Classes/PCP/Internal/DegreeReduction.lean | 313 +++++ .../PCP/Internal/DegreeReductionSound.lean | 294 +++++ Complexitylib/Classes/PCP/Internal/Dinur.lean | 361 ++++++ .../Classes/PCP/Internal/EdgeExpansion.lean | 142 ++ .../Classes/PCP/Internal/Expander.lean | 140 ++ .../Classes/PCP/Internal/ExpanderExists.lean | 112 ++ .../Classes/PCP/Internal/ExpanderMerge.lean | 155 +++ .../Classes/PCP/Internal/ExpanderPad.lean | 260 ++++ .../Classes/PCP/Internal/ExpanderRandom.lean | 216 +++ .../Classes/PCP/Internal/ExpanderizeCSP.lean | 171 +++ .../Classes/PCP/Internal/FamilyFin.lean | 386 ++++++ .../Classes/PCP/Internal/FiniteKey.lean | 91 ++ .../Classes/PCP/Internal/FinsetPlurality.lean | 55 + .../Classes/PCP/Internal/GapReduction.lean | 68 + .../PCP/Internal/GuessVerifyGeneric.lean | 163 +++ .../Classes/PCP/Internal/Hadamard.lean | 535 ++++++++ .../Classes/PCP/Internal/HadamardTester.lean | 275 ++++ .../Classes/PCP/Internal/KilledCSP.lean | 229 ++++ .../PCP/Internal/KilledFirstMoment.lean | 1155 +++++++++++++++++ .../Classes/PCP/Internal/KilledPlurality.lean | 290 +++++ .../Classes/PCP/Internal/KilledWalk.lean | 388 ++++++ .../Classes/PCP/Internal/ListEncode.lean | 169 +++ .../Classes/PCP/Internal/LocalTest.lean | 298 +++++ .../Classes/PCP/Internal/Materialize.lean | 283 ++++ .../Classes/PCP/Internal/MaxLoop.lean | 169 +++ .../Classes/PCP/Internal/MergeGen.lean | 486 +++++++ .../Classes/PCP/Internal/Mixing.lean | 188 +++ .../Classes/PCP/Internal/NatEncode.lean | 201 +++ .../Classes/PCP/Internal/NumEnc.lean | 339 +++++ .../Classes/PCP/Internal/NumEncPi.lean | 176 +++ .../Classes/PCP/Internal/PCPtoSAT.lean | 145 +++ .../Classes/PCP/Internal/PadGraph.lean | 118 ++ .../Classes/PCP/Internal/PermArith.lean | 254 ++++ .../Classes/PCP/Internal/PermCount.lean | 212 +++ .../Classes/PCP/Internal/PermGraph.lean | 145 +++ .../Classes/PCP/Internal/PosScan.lean | 142 ++ .../Classes/PCP/Internal/PositionsFP.lean | 112 ++ Complexitylib/Classes/PCP/Internal/Power.lean | 198 +++ .../Classes/PCP/Internal/PoweringBound.lean | 419 ++++++ .../Classes/PCP/Internal/Preprocess.lean | 196 +++ .../Classes/PCP/Internal/RankCount.lean | 131 ++ .../Classes/PCP/Internal/RegCSP.lean | 176 +++ .../Classes/PCP/Internal/RegularGraph.lean | 249 ++++ .../Classes/PCP/Internal/SecondMoment.lean | 87 ++ .../Classes/PCP/Internal/SelfLoops.lean | 185 +++ .../Classes/PCP/Internal/SquareVerifier.lean | 349 +++++ .../Classes/PCP/Internal/StripTrailing.lean | 171 +++ .../Classes/PCP/Internal/SubsetNP.lean | 327 +++++ .../Classes/PCP/Internal/SubsetNPFinal.lean | 157 +++ .../Classes/PCP/Internal/SymbolCodec.lean | 62 + .../Classes/PCP/Internal/TesterChecks.lean | 237 ++++ .../Classes/PCP/Internal/TesterCore.lean | 294 +++++ .../PCP/Internal/ThreeSATReduction.lean | 273 ++++ .../Classes/PCP/Internal/TowerFin.lean | 476 +++++++ .../Classes/PCP/Internal/TowerTable.lean | 326 +++++ .../Classes/PCP/Internal/UnaryDivMod.lean | 270 ++++ .../Classes/PCP/Internal/UnaryExp.lean | 115 ++ .../Classes/PCP/Internal/UnaryList.lean | 420 ++++++ Complexitylib/Classes/PCP/Internal/Union.lean | 212 +++ .../Classes/PCP/Internal/VerifierLang.lean | 268 ++++ Complexitylib/Classes/PCP/Internal/Walk.lean | 95 ++ .../Classes/PCP/Internal/WalkDart.lean | 243 ++++ .../Classes/PCP/Internal/WalkPath.lean | 143 ++ .../Classes/PCP/Internal/WalkSplit.lean | 324 +++++ .../Classes/PCP/Internal/ZigZag.lean | 783 +++++++++++ .../PCP/Internal/ZigZagBaseExists.lean | 85 ++ .../Classes/PCP/Internal/ZigZagTower.lean | 133 ++ Complexitylib/Encoding/Pairing.lean | 10 + Complexitylib/Languages/Contains.lean | 8 + 137 files changed, 32013 insertions(+), 41 deletions(-) create mode 100644 Complexitylib/Classes/P/Cobham/Internal/BinValLE.lean create mode 100644 Complexitylib/Classes/P/Cobham/Internal/BumpBits.lean create mode 100644 Complexitylib/Classes/P/Cobham/Internal/CodeAccept.lean create mode 100644 Complexitylib/Classes/P/Cobham/Internal/CodeRewind.lean create mode 100644 Complexitylib/Classes/P/Cobham/Internal/CodeStep.lean create mode 100644 Complexitylib/Classes/P/Cobham/Internal/FPBridge.lean create mode 100644 Complexitylib/Classes/P/Cobham/Internal/HeadOps.lean create mode 100644 Complexitylib/Classes/P/Cobham/Internal/PVerdict.lean create mode 100644 Complexitylib/Classes/P/Cobham/Internal/PolyLen.lean create mode 100644 Complexitylib/Classes/P/Cobham/Internal/PolyRuler.lean create mode 100644 Complexitylib/Classes/P/Cobham/Internal/StringOps.lean create mode 100644 Complexitylib/Classes/P/DecisionFn.lean create mode 100644 Complexitylib/Classes/PCP.lean create mode 100644 Complexitylib/Classes/PCP/Defs.lean create mode 100644 Complexitylib/Classes/PCP/Internal.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgCSPModel.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgCompose.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgEdge.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgFamily.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgFormula.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgGap.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgGapAll.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgGapCSP.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgGraph.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgInit.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgIter.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgKey.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgKeyFn.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgKilled.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgLevel.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgLog.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgPCP.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgPosNum.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgPreRel.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgPreRot.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgPreprocess.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgRound.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgStep.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgUniform.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlgWalk.lean create mode 100644 Complexitylib/Classes/PCP/Internal/AlphabetLift.lean create mode 100644 Complexitylib/Classes/PCP/Internal/Amplification.lean create mode 100644 Complexitylib/Classes/PCP/Internal/Arithmetize.lean create mode 100644 Complexitylib/Classes/PCP/Internal/BaseAlg.lean create mode 100644 Complexitylib/Classes/PCP/Internal/BinToUnary.lean create mode 100644 Complexitylib/Classes/PCP/Internal/BitwiseFP.lean create mode 100644 Complexitylib/Classes/PCP/Internal/BoundedQuant.lean create mode 100644 Complexitylib/Classes/PCP/Internal/CNFCount.lean create mode 100644 Complexitylib/Classes/PCP/Internal/CNFMaxVar.lean create mode 100644 Complexitylib/Classes/PCP/Internal/CNFSegment.lean create mode 100644 Complexitylib/Classes/PCP/Internal/CNFTokens.lean create mode 100644 Complexitylib/Classes/PCP/Internal/CSPVerifier.lean create mode 100644 Complexitylib/Classes/PCP/Internal/Cheeger.lean create mode 100644 Complexitylib/Classes/PCP/Internal/CloudCount.lean create mode 100644 Complexitylib/Classes/PCP/Internal/CloudDisagreement.lean create mode 100644 Complexitylib/Classes/PCP/Internal/CoinEnum.lean create mode 100644 Complexitylib/Classes/PCP/Internal/Compose.lean create mode 100644 Complexitylib/Classes/PCP/Internal/ConsistencyLang.lean create mode 100644 Complexitylib/Classes/PCP/Internal/ConstraintGraph.lean create mode 100644 Complexitylib/Classes/PCP/Internal/CubeBlocks.lean create mode 100644 Complexitylib/Classes/PCP/Internal/DartCorrelation.lean create mode 100644 Complexitylib/Classes/PCP/Internal/DataScan.lean create mode 100644 Complexitylib/Classes/PCP/Internal/DataScanSpec.lean create mode 100644 Complexitylib/Classes/PCP/Internal/DegreeReduction.lean create mode 100644 Complexitylib/Classes/PCP/Internal/DegreeReductionSound.lean create mode 100644 Complexitylib/Classes/PCP/Internal/Dinur.lean create mode 100644 Complexitylib/Classes/PCP/Internal/EdgeExpansion.lean create mode 100644 Complexitylib/Classes/PCP/Internal/Expander.lean create mode 100644 Complexitylib/Classes/PCP/Internal/ExpanderExists.lean create mode 100644 Complexitylib/Classes/PCP/Internal/ExpanderMerge.lean create mode 100644 Complexitylib/Classes/PCP/Internal/ExpanderPad.lean create mode 100644 Complexitylib/Classes/PCP/Internal/ExpanderRandom.lean create mode 100644 Complexitylib/Classes/PCP/Internal/ExpanderizeCSP.lean create mode 100644 Complexitylib/Classes/PCP/Internal/FamilyFin.lean create mode 100644 Complexitylib/Classes/PCP/Internal/FiniteKey.lean create mode 100644 Complexitylib/Classes/PCP/Internal/FinsetPlurality.lean create mode 100644 Complexitylib/Classes/PCP/Internal/GapReduction.lean create mode 100644 Complexitylib/Classes/PCP/Internal/GuessVerifyGeneric.lean create mode 100644 Complexitylib/Classes/PCP/Internal/Hadamard.lean create mode 100644 Complexitylib/Classes/PCP/Internal/HadamardTester.lean create mode 100644 Complexitylib/Classes/PCP/Internal/KilledCSP.lean create mode 100644 Complexitylib/Classes/PCP/Internal/KilledFirstMoment.lean create mode 100644 Complexitylib/Classes/PCP/Internal/KilledPlurality.lean create mode 100644 Complexitylib/Classes/PCP/Internal/KilledWalk.lean create mode 100644 Complexitylib/Classes/PCP/Internal/ListEncode.lean create mode 100644 Complexitylib/Classes/PCP/Internal/LocalTest.lean create mode 100644 Complexitylib/Classes/PCP/Internal/Materialize.lean create mode 100644 Complexitylib/Classes/PCP/Internal/MaxLoop.lean create mode 100644 Complexitylib/Classes/PCP/Internal/MergeGen.lean create mode 100644 Complexitylib/Classes/PCP/Internal/Mixing.lean create mode 100644 Complexitylib/Classes/PCP/Internal/NatEncode.lean create mode 100644 Complexitylib/Classes/PCP/Internal/NumEnc.lean create mode 100644 Complexitylib/Classes/PCP/Internal/NumEncPi.lean create mode 100644 Complexitylib/Classes/PCP/Internal/PCPtoSAT.lean create mode 100644 Complexitylib/Classes/PCP/Internal/PadGraph.lean create mode 100644 Complexitylib/Classes/PCP/Internal/PermArith.lean create mode 100644 Complexitylib/Classes/PCP/Internal/PermCount.lean create mode 100644 Complexitylib/Classes/PCP/Internal/PermGraph.lean create mode 100644 Complexitylib/Classes/PCP/Internal/PosScan.lean create mode 100644 Complexitylib/Classes/PCP/Internal/PositionsFP.lean create mode 100644 Complexitylib/Classes/PCP/Internal/Power.lean create mode 100644 Complexitylib/Classes/PCP/Internal/PoweringBound.lean create mode 100644 Complexitylib/Classes/PCP/Internal/Preprocess.lean create mode 100644 Complexitylib/Classes/PCP/Internal/RankCount.lean create mode 100644 Complexitylib/Classes/PCP/Internal/RegCSP.lean create mode 100644 Complexitylib/Classes/PCP/Internal/RegularGraph.lean create mode 100644 Complexitylib/Classes/PCP/Internal/SecondMoment.lean create mode 100644 Complexitylib/Classes/PCP/Internal/SelfLoops.lean create mode 100644 Complexitylib/Classes/PCP/Internal/SquareVerifier.lean create mode 100644 Complexitylib/Classes/PCP/Internal/StripTrailing.lean create mode 100644 Complexitylib/Classes/PCP/Internal/SubsetNP.lean create mode 100644 Complexitylib/Classes/PCP/Internal/SubsetNPFinal.lean create mode 100644 Complexitylib/Classes/PCP/Internal/SymbolCodec.lean create mode 100644 Complexitylib/Classes/PCP/Internal/TesterChecks.lean create mode 100644 Complexitylib/Classes/PCP/Internal/TesterCore.lean create mode 100644 Complexitylib/Classes/PCP/Internal/ThreeSATReduction.lean create mode 100644 Complexitylib/Classes/PCP/Internal/TowerFin.lean create mode 100644 Complexitylib/Classes/PCP/Internal/TowerTable.lean create mode 100644 Complexitylib/Classes/PCP/Internal/UnaryDivMod.lean create mode 100644 Complexitylib/Classes/PCP/Internal/UnaryExp.lean create mode 100644 Complexitylib/Classes/PCP/Internal/UnaryList.lean create mode 100644 Complexitylib/Classes/PCP/Internal/Union.lean create mode 100644 Complexitylib/Classes/PCP/Internal/VerifierLang.lean create mode 100644 Complexitylib/Classes/PCP/Internal/Walk.lean create mode 100644 Complexitylib/Classes/PCP/Internal/WalkDart.lean create mode 100644 Complexitylib/Classes/PCP/Internal/WalkPath.lean create mode 100644 Complexitylib/Classes/PCP/Internal/WalkSplit.lean create mode 100644 Complexitylib/Classes/PCP/Internal/ZigZag.lean create mode 100644 Complexitylib/Classes/PCP/Internal/ZigZagBaseExists.lean create mode 100644 Complexitylib/Classes/PCP/Internal/ZigZagTower.lean diff --git a/Complexitylib/Asymptotics/PolyBound.lean b/Complexitylib/Asymptotics/PolyBound.lean index e4b24aab..32f98524 100644 --- a/Complexitylib/Asymptotics/PolyBound.lean +++ b/Complexitylib/Asymptotics/PolyBound.lean @@ -23,6 +23,7 @@ form the complexity classes are stated in. - `PolyBound.const`, `.id`, `.add`, `.mul`, `.pow`, `.mono`, `.max`, `.eval` — the closure API - `PolyBound.bigO` — a polynomial bound is a big-O power bound +- `PolyBound.exists_mul_pow_bound` — a polynomial bound is an `A * (n + 1) ^ B` bound -/ @@ -83,6 +84,21 @@ theorem bigO {f : ℕ → ℕ} (hf : PolyBound f) : ∃ d, f =O (· ^ d) := by obtain ⟨p, hp⟩ := hf exact ⟨p.natDegree, BigO.of_polynomial_bound p hp⟩ +/-- A polynomial bound is a bound of the form `A * (n + 1) ^ B`: take `A` to be +the sum of the coefficients and `B` the degree. -/ +theorem exists_mul_pow_bound {f : ℕ → ℕ} (hf : PolyBound f) : + ∃ A B : ℕ, ∀ n, f n ≤ A * (n + 1) ^ B := by + obtain ⟨p, hp⟩ := hf + refine ⟨∑ i ∈ Finset.range (p.natDegree + 1), p.coeff i, p.natDegree, fun n => ?_⟩ + refine le_trans (hp n) ?_ + rw [Polynomial.eval_eq_sum_range, Finset.sum_mul] + refine Finset.sum_le_sum fun i hi => ?_ + have hi' : i ≤ p.natDegree := by + rw [Finset.mem_range] at hi + omega + exact Nat.mul_le_mul_left _ + (le_trans (Nat.pow_le_pow_left (by omega) i) (Nat.pow_le_pow_right (by omega) hi')) + end PolyBound end Complexity diff --git a/Complexitylib/Classes.lean b/Complexitylib/Classes.lean index f679cb25..20b65a26 100644 --- a/Complexitylib/Classes.lean +++ b/Complexitylib/Classes.lean @@ -68,6 +68,7 @@ public import Complexitylib.Classes.L.PolynomialTime public import Complexitylib.Classes.Exponential public import Complexitylib.Classes.DTISP public import Complexitylib.Classes.Containments +public import Complexitylib.Classes.PCP public import Complexitylib.Classes.Hierarchy import Complexitylib.Classes.NP.Internal.PairSplitTM diff --git a/Complexitylib/Classes/P/Cobham.lean b/Complexitylib/Classes/P/Cobham.lean index 9acc095e..b3237d46 100644 --- a/Complexitylib/Classes/P/Cobham.lean +++ b/Complexitylib/Classes/P/Cobham.lean @@ -79,6 +79,10 @@ arity one. -/ theorem CobhamFP_subset_FP : CobhamFP ⊆ FP := Cobham.CobhamFP_subset_FP_of_FPn +/-- Every constant function is polynomial-time. -/ +theorem constFn_mem_FP (c : List Bool) : (fun _ : List Bool => c) ∈ FP := + CobhamFP_subset_FP (Cobham.const c) + /-- Cobham's algebra is complete for polynomial time: every polynomial-time computable function belongs to the algebra. diff --git a/Complexitylib/Classes/P/Cobham/Internal.lean b/Complexitylib/Classes/P/Cobham/Internal.lean index 43232ffc..abca5b35 100644 --- a/Complexitylib/Classes/P/Cobham/Internal.lean +++ b/Complexitylib/Classes/P/Cobham/Internal.lean @@ -13,6 +13,8 @@ public import Complexitylib.Classes.P.Cobham.Internal.ConsBit public import Complexitylib.Classes.P.Cobham.Internal.Reorder public import Complexitylib.Classes.P.Cobham.Internal.Vec public import Complexitylib.Classes.P.Cobham.Internal.Algebra +public import Complexitylib.Classes.P.Cobham.Internal.PolyLen +public import Complexitylib.Classes.P.Cobham.Internal.StringOps public import Complexitylib.Classes.P.Cobham.Internal.Encoding public import Complexitylib.Classes.P.Cobham.Internal.StepAlgebra public import Complexitylib.Classes.P.Cobham.Internal.Simulate diff --git a/Complexitylib/Classes/P/Cobham/Internal/BinValLE.lean b/Complexitylib/Classes/P/Cobham/Internal/BinValLE.lean new file mode 100644 index 00000000..4f21a7b1 --- /dev/null +++ b/Complexitylib/Classes/P/Cobham/Internal/BinValLE.lean @@ -0,0 +1,129 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Mathlib.Data.Nat.Bits +public import Complexitylib.Classes.P.Cobham + +/-! +# Bounded binary values + +`binValLE` reads a bit list as a binary number capped at a length bound, and +`bitsOfLenLE` is its inverse on the range: the fixed-width binary expansion of a +number. Together they enumerate the `2 ^ n` strings of length `n` by number, +which is how the PCP verifier's coin tosses are indexed. + +## Main definitions + +- `binValLE`, `bitsOfLenLE` — the value of a bit list, and the bits of a value +- `binValLE_bitsOfLenLE`, `bitsOfLenLE_binValLE` — the two round trips +-/ + +@[expose] public section + +namespace Complexity + +/-- The little-endian value of a bitstring. -/ +def binValLE : List Bool → ℕ + | [] => 0 + | b :: w => (if b then 1 else 0) + 2 * binValLE w + +/-- The little-endian bitstring of a given length and value. -/ +def bitsOfLenLE : ℕ → ℕ → List Bool + | 0, _ => [] + | ℓ + 1, v => decide (v % 2 = 1) :: bitsOfLenLE ℓ (v / 2) + +@[simp] theorem bitsOfLenLE_length (ℓ v : ℕ) : (bitsOfLenLE ℓ v).length = ℓ := by + induction ℓ generalizing v with + | zero => rfl + | succ ℓ ih => simp [bitsOfLenLE, ih] + +theorem binValLE_lt (w : List Bool) : binValLE w < 2 ^ w.length := by + induction w with + | nil => simp [binValLE] + | cons b w ih => + simp only [binValLE, List.length_cons, pow_succ] + cases b <;> simp <;> omega + +/-- The round trip, one way. -/ +theorem bitsOfLenLE_binValLE (w : List Bool) : bitsOfLenLE w.length (binValLE w) = w := by + induction w with + | nil => rfl + | cons b w ih => + have hmod : binValLE (b :: w) % 2 = if b then 1 else 0 := by + simp only [binValLE] + cases b <;> simp [Nat.add_mul_mod_self_left] + have hdiv : binValLE (b :: w) / 2 = binValLE w := by + simp only [binValLE] + cases b <;> simp [Nat.add_mul_div_left] + simp only [bitsOfLenLE, hmod, hdiv, ih] + cases b <;> simp + +/-- The round trip, the other way. -/ +theorem binValLE_bitsOfLenLE : ∀ (ℓ v : ℕ), v < 2 ^ ℓ → binValLE (bitsOfLenLE ℓ v) = v := by + intro ℓ + induction ℓ with + | zero => + intro v hv + simp only [pow_zero] at hv + simp [bitsOfLenLE, binValLE] + omega + | succ ℓ ih => + intro v hv + have hhalf : v / 2 < 2 ^ ℓ := by + have : (2 : ℕ) ^ (ℓ + 1) = 2 ^ ℓ * 2 := by ring + omega + have hdm : 2 * (v / 2) + v % 2 = v := by omega + simp only [bitsOfLenLE, binValLE, ih _ hhalf] + by_cases hb : v % 2 = 1 + · simp [hb] + omega + · simp [hb] + omega + +/-- **Each bit of the enumeration is a bit of the counter.** The `j`-th entry of the length-`ℓ` +little-endian string for `v` is bit `j` of `v`. This is the form in which the correspondence meets +a tape: whatever encoding a counter tape uses, its `j`-th cell holds this bit — and cells beyond +the counter's own digits read as `false`, which is bit `j` of `v` too. -/ +theorem bitsOfLenLE_getElem : + ∀ (ℓ v j : ℕ) (h : j < ℓ), (bitsOfLenLE ℓ v)[j]'(by simpa using h) + = decide (v / 2 ^ j % 2 = 1) := by + intro ℓ + induction ℓ with + | zero => intro v j h; omega + | succ ℓ ih => + intro v j h + cases j with + | zero => simp [bitsOfLenLE] + | succ j => + have hj : j < ℓ := by omega + have hstep := ih (v / 2) j hj + simp only [bitsOfLenLE, List.getElem_cons_succ] + rw [hstep, Nat.div_div_eq_div_mul, pow_succ, Nat.mul_comm] + +/-- **The canonical representation reads back as its value.** -/ +theorem binValLE_bits : ∀ n : ℕ, binValLE n.bits = n := by + intro n + induction n using Nat.strong_induction_on with + | _ n ih => + match n, ih with + | 0, _ => rfl + | (m + 1), ih => + rcases Nat.even_or_odd (m + 1) with ⟨q, hq⟩ | ⟨q, hq⟩ + · have hq0 : q ≠ 0 := by omega + have h2 : m + 1 = 2 * q := by omega + rw [h2, Nat.bit0_bits q hq0, binValLE, ih q (by omega)] + simp + · have h2 : m + 1 = 2 * q + 1 := by omega + rw [h2, Nat.bit1_bits q, binValLE, ih q (by omega)] + simp + omega + +theorem bitsOfLenLE_zero (ℓ : ℕ) : bitsOfLenLE ℓ 0 = List.replicate ℓ false := by + induction ℓ with + | zero => rfl + | succ ℓ ih => rw [bitsOfLenLE, ih, List.replicate_succ]; simp + +end Complexity diff --git a/Complexitylib/Classes/P/Cobham/Internal/BumpBits.lean b/Complexitylib/Classes/P/Cobham/Internal/BumpBits.lean new file mode 100644 index 00000000..539fd0db --- /dev/null +++ b/Complexitylib/Classes/P/Cobham/Internal/BumpBits.lean @@ -0,0 +1,300 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P.Cobham.Internal.BinValLE + +/-! +# Incrementing a fixed-width counter in polynomial time + +`bumpBits` adds one to a bit list of fixed width, and `bumpCode` packs the +carry state so that the increment can be run by `Cobham.iterate_mem_FP`. The +value lemmas relate the result to `binValLE`, and `bumpCodeFn_mem_FP` puts the +whole step in `FP`. Used to enumerate coin tosses in order. + +## Main definitions + +- `addBit`, `bumpBits`, `bumpOver` — the increment and its overflow flag +- `bumpStep`, `bumpPack`, `bumpRun`, `bumpCode` — the packed iteration +- `bumpCodeFn_mem_FP` — the packed step is polynomial-time +-/ + +@[expose] public section + +namespace Complexity +open Cobham + +/-- Add a carry bit into a little-endian bitstring: the carry out and the result. -/ +def addBit : Bool → List Bool → Bool × List Bool + | c, [] => (c, []) + | c, b :: t => ((addBit (c && b) t).1, (xor c b) :: (addBit (c && b) t).2) + +@[simp] theorem addBit_nil (c : Bool) : addBit c [] = (c, []) := rfl + +@[simp] theorem addBit_cons (c b : Bool) (t : List Bool) : + addBit c (b :: t) = ((addBit (c && b) t).1, (xor c b) :: (addBit (c && b) t).2) := rfl + +/-- The increment of a little-endian bitstring, wrapping on overflow. -/ +def bumpBits (w : List Bool) : List Bool := (addBit true w).2 + +/-- Did the increment wrap around? -/ +def bumpOver (w : List Bool) : Bool := (addBit true w).1 + +/-- Adding no carry changes nothing. -/ +@[simp] theorem addBit_false (w : List Bool) : addBit false w = (false, w) := by + induction w with + | nil => rfl + | cons b t ih => simp [addBit, ih] + +/-- The width is preserved. -/ +@[simp] theorem addBit_length (c : Bool) (w : List Bool) : + (addBit c w).2.length = w.length := by + induction w generalizing c with + | nil => rfl + | cons b t ih => simp [addBit, ih] + +@[simp] theorem bumpBits_length (w : List Bool) : (bumpBits w).length = w.length := + addBit_length true w + +/-- **The adder is correct.** -/ +theorem addBit_binValLE (c : Bool) (w : List Bool) : + binValLE (addBit c w).2 + (if (addBit c w).1 then 2 ^ w.length else 0) + = binValLE w + (if c then 1 else 0) := by + induction w generalizing c with + | nil => cases c <;> simp [binValLE] + | cons b t ih => + have h := ih (c && b) + rcases hab : addBit (c && b) t with ⟨d, s⟩ + rw [hab] at h + simp only [addBit_cons, hab, binValLE, List.length_cons, pow_succ] + clear hab ih + cases c <;> cases b <;> cases d <;> simp at h ⊢ <;> omega + +/-- **The carry fires exactly on the last candidate.** -/ +theorem bumpOver_iff (w : List Bool) : + bumpOver w = true ↔ binValLE w = 2 ^ w.length - 1 := by + have h := addBit_binValLE true w + have hlt := binValLE_lt (addBit true w).2 + have hlt' := binValLE_lt w + rw [addBit_length] at hlt + rw [bumpOver] + constructor + · intro hc + rw [hc] at h + simp at h + omega + · intro hv + by_contra hc + simp only [Bool.not_eq_true] at hc + rw [hc] at h + simp at h + omega + +/-- **The increment counts.** -/ +theorem binValLE_bumpBits_of_not_over (w : List Bool) (h : bumpOver w = false) : + binValLE (bumpBits w) = binValLE w + 1 := by + have hb := addBit_binValLE true w + rw [bumpOver] at h + rw [h] at hb + simp at hb + rw [bumpBits] + omega + +/-- Iterating the increment from zero enumerates the candidates in order. -/ +theorem bumpBits_iterate (ℓ : ℕ) : + ∀ j, j < 2 ^ ℓ → bumpBits^[j] (bitsOfLenLE ℓ 0) = bitsOfLenLE ℓ j := by + intro j + induction j with + | zero => intro _; rfl + | succ j ih => + intro hj + have hj' : j < 2 ^ ℓ := by omega + rw [Function.iterate_succ_apply', ih hj'] + have hlen : (bitsOfLenLE ℓ j).length = ℓ := bitsOfLenLE_length ℓ j + have hval : binValLE (bitsOfLenLE ℓ j) = j := binValLE_bitsOfLenLE ℓ j hj' + have hover : bumpOver (bitsOfLenLE ℓ j) = false := by + by_contra hc + simp only [Bool.not_eq_false] at hc + rw [bumpOver_iff, hlen, hval] at hc + omega + have := binValLE_bumpBits_of_not_over _ hover + rw [hval] at this + rw [← bitsOfLenLE_binValLE (bumpBits (bitsOfLenLE ℓ j)), this, bumpBits_length, hlen] + +/-- One step of the increment scan. -/ +def bumpStep : List Bool × List Bool × List Bool → List Bool × List Bool × List Bool + | (c, acc, []) => (c, acc, []) + | (c, acc, b :: t) => (andBit c [b], acc ++ selectHead c (notBit [b]) [b], t) + +theorem bumpStep_flag (c b : Bool) (acc t : List Bool) : + bumpStep ([c], acc, b :: t) = ([c && b], acc ++ [xor c b], t) := by + rw [bumpStep] + refine Prod.ext ?_ (Prod.ext ?_ rfl) + · show andBit [c] [b] = [c && b] + cases c <;> cases b <;> rfl + · show acc ++ selectHead [c] (notBit [b]) [b] = acc ++ [xor c b] + cases c <;> cases b <;> rfl + +/-- The scan never has more in hand than it started with. -/ +@[simp] theorem bumpStep_nil (c acc : List Bool) : bumpStep (c, acc, []) = (c, acc, []) := rfl + +/-- **The scan computes the increment.** -/ +theorem bumpStep_iterate_run (c : Bool) (acc w : List Bool) : + bumpStep^[w.length] ([c], acc, w) = ([(addBit c w).1], acc ++ (addBit c w).2, []) := by + induction w generalizing c acc with + | nil => simp + | cons b t ih => + rw [List.length_cons, Function.iterate_succ_apply, bumpStep_flag, ih, + addBit_cons] + simp + +theorem bumpStep_iterate_length (c : Bool) (acc w : List Bool) (n : ℕ) : + (bumpStep^[n] ([c], acc, w)).1.length = 1 ∧ + (bumpStep^[n] ([c], acc, w)).2.1.length + (bumpStep^[n] ([c], acc, w)).2.2.length + ≤ acc.length + w.length := by + induction n generalizing c acc w with + | zero => exact ⟨rfl, le_rfl⟩ + | succ n ih => + rw [Function.iterate_succ_apply] + cases w with + | nil => + have := ih c acc [] + simpa using this + | cons b t => + rw [bumpStep_flag] + have := ih (c && b) (acc ++ [xor c b]) t + refine ⟨this.1, le_trans this.2 ?_⟩ + simp + omega + +/-- The packed scan state. -/ +def bumpPack (c acc rest : List Bool) : List Bool := pair c (pair acc rest) + +@[simp] theorem bumpPack_length (c acc rest : List Bool) : + (bumpPack c acc rest).length = 2 * c.length + 2 * acc.length + rest.length + 4 := by + rw [bumpPack, pair_length, pair_length] + omega + +/-- One step of the packed scan. -/ +def bumpStepP (z : List Bool) : List Bool := + selectHead (lenLeFlag (pairSnd (pairSnd z)) [false]) + (pair (andBit (pairFst z) ((pairSnd (pairSnd z)).take 1)) + (pair (pairFst (pairSnd z) ++ + selectHead (pairFst z) (notBit ((pairSnd (pairSnd z)).take 1)) + ((pairSnd (pairSnd z)).take 1)) + ((pairSnd (pairSnd z)).drop 1))) + z + +/-- **The packed step is the unpacked step.** -/ +theorem bumpStepP_pack (c acc rest : List Bool) : + bumpStepP (bumpPack c acc rest) + = bumpPack (bumpStep (c, acc, rest)).1 (bumpStep (c, acc, rest)).2.1 + (bumpStep (c, acc, rest)).2.2 := by + rw [bumpStepP, bumpPack] + simp only [pairFst_pair, pairSnd_pair] + cases rest with + | nil => + rw [selectHead] + have hflag : lenLeFlag ([] : List Bool) [false] = [false] := rfl + rw [hflag] + simp [bumpPack] + | cons b t => + have hflag : lenLeFlag (b :: t) [false] = [true] := + (lenLeFlag_eq_true_iff (b :: t) [false]).mpr (by simp) + rw [selectHead, hflag] + simp only [List.head?_cons, reduceIte] + rw [bumpStep, bumpPack] + simp + +/-- **The packed iteration is the unpacked one.** -/ +theorem bumpStepP_iterate (s : List Bool × List Bool × List Bool) (n : ℕ) : + bumpStepP^[n] (bumpPack s.1 s.2.1 s.2.2) + = bumpPack (bumpStep^[n] s).1 (bumpStep^[n] s).2.1 (bumpStep^[n] s).2.2 := by + induction n generalizing s with + | zero => rfl + | succ n ih => + rw [Function.iterate_succ_apply, bumpStepP_pack, ih (bumpStep s), + Function.iterate_succ_apply] + +/-- The packed iteration, with the state's three components spelled out. -/ +theorem bumpStepP_iterate_args (c acc rest : List Bool) (n : ℕ) : + bumpStepP^[n] (bumpPack c acc rest) + = bumpPack (bumpStep^[n] (c, acc, rest)).1 (bumpStep^[n] (c, acc, rest)).2.1 + (bumpStep^[n] (c, acc, rest)).2.2 := + bumpStepP_iterate (c, acc, rest) n + +/-- The packed scan run to completion. -/ +def bumpRun (w : List Bool) : List Bool := bumpStepP^[w.length] (bumpPack [true] [] w) + +/-- The increment of `w`, computed by the scan. -/ +def bumpCode (w : List Bool) : List Bool := pairFst (pairSnd (bumpRun w)) + +theorem bumpRun_eq (w : List Bool) : + bumpRun w = bumpPack [bumpOver w] (bumpBits w) [] := by + rw [bumpRun, bumpStepP_iterate_args, bumpStep_iterate_run, bumpOver, bumpBits] + simp + +@[simp] theorem bumpCode_eq (w : List Bool) : bumpCode w = bumpBits w := by + rw [bumpCode, bumpRun_eq, bumpPack] + simp + +theorem bumpStepP_mem_FP : bumpStepP ∈ FP := by + have hid : (fun z : List Bool => z) ∈ FP := CobhamFP_subset_FP (Cobham.proj 0) + have hfst : ∀ {a : List Bool → List Bool}, a ∈ FP → + (fun z => pairFst (a z)) ∈ FP := by + intro a ha + have := mem_FP_comp ha Cobham.fstBlock_mem_FP + simpa [Function.comp] using this + have hsnd : ∀ {a : List Bool → List Bool}, a ∈ FP → + (fun z => pairSnd (a z)) ∈ FP := by + intro a ha + have := mem_FP_comp ha Cobham.sndBlock_mem_FP + simpa [Function.comp] using this + have hc := hfst hid + have hw := hsnd hid + have hrest := hsnd hw + have hacc := hfst hw + have hone : (fun _ : List Bool => ([false] : List Bool)) ∈ FP := constFn_mem_FP [false] + have htake : (fun z => (pairSnd (pairSnd z)).take 1) ∈ FP := by + have := Cobham.takeLenFn_mem_FP hone hrest + simpa using this + have hdrop : (fun z => (pairSnd (pairSnd z)).drop 1) ∈ FP := by + have := dropLenFn_mem_FP hone hrest + simpa using this + have hbit : (fun z => selectHead (pairFst z) + (notBit ((pairSnd (pairSnd z)).take 1)) ((pairSnd (pairSnd z)).take 1)) ∈ FP := + Cobham.selectHeadFn_mem_FP hc (notBitFn_mem_FP htake) htake + exact Cobham.selectHeadFn_mem_FP (lenLeFlagFn_mem_FP hrest hone) + (Cobham.pairFn_mem_FP (andBitFn_mem_FP hc htake) + (Cobham.pairFn_mem_FP (Cobham.appendFn_mem_FP hacc hbit) hdrop)) hid + +/-- **The increment is polynomial-time.** -/ +theorem bumpRunFn_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => bumpRun (a z)) ∈ FP := by + have hinit : (fun z => bumpPack [true] [] (a z)) ∈ FP := + Cobham.pairFn_mem_FP (constFn_mem_FP [true]) + (Cobham.pairFn_mem_FP (constFn_mem_FP []) ha) + have hwidth : (fun z => pair [true] (pair (a z) (a z))) ∈ FP := + Cobham.pairFn_mem_FP (constFn_mem_FP [true]) (Cobham.pairFn_mem_FP ha ha) + have hbound : ∀ z, ∀ n ≤ (a z).length, + (bumpStepP^[n] (bumpPack [true] [] (a z))).length + ≤ (pair [true] (pair (a z) (a z))).length := by + intro z n _ + rw [bumpStepP_iterate_args] + obtain ⟨h1, h2⟩ := bumpStep_iterate_length true [] (a z) n + have hone : ([true] : List Bool).length = 1 := rfl + rw [bumpPack_length, pair_length, pair_length] + simp only [List.length_nil] at h2 + omega + exact Cobham.iterate_mem_FP bumpStepP_mem_FP hinit ha hwidth hbound + +theorem bumpCodeFn_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => bumpCode (a z)) ∈ FP := by + have h1 := mem_FP_comp (bumpRunFn_mem_FP ha) Cobham.sndBlock_mem_FP + have h2 := mem_FP_comp h1 Cobham.fstBlock_mem_FP + simpa [Function.comp, bumpCode] using h2 + +end Complexity diff --git a/Complexitylib/Classes/P/Cobham/Internal/CodeAccept.lean b/Complexitylib/Classes/P/Cobham/Internal/CodeAccept.lean new file mode 100644 index 00000000..41ffac1d --- /dev/null +++ b/Complexitylib/Classes/P/Cobham/Internal/CodeAccept.lean @@ -0,0 +1,91 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.CodeRewind + +/-! +# Deciding whether a record is accepting + +⚠️ Unreviewed by Bolton + +A record is accepting when its state field is the halting state and its output +tape holds `1` in cell `1`. The state field is block `0` truncated to the state +code's width, and the verdict cell is read after the rewind of +`Complexitylib.Classes.P.Cobham.Internal.CodeRewind`. + +## Main definitions + +- `Complexity.verdictSym` — the symbol in cell `1` of a rewound output code +- `Complexity.acceptFlag` — the accepting-record test + +## Main results + +- `Complexity.verdictSym_rewound` — what the verdict cell reads +- `Complexity.acceptFlagFn_mem_FP` — the test is polynomial-time +-/ + +@[expose] public section + +namespace Complexity + +open Cobham + +variable {k : ℕ} + +/-! ## The verdict cell -/ + +/-- The symbol in cell `1` of a rewound output code. -/ +def verdictSym (R z : List Bool) : List Bool := ((z.drop R.length).drop 2).take 2 + +/-- **The verdict cell of a rewound output code is cell `1` of the tape.** -/ +theorem verdictSym_rewound (W : ℕ) (t : Tape) (hW : 1 ≤ W) : + verdictSym (blockRuler W) (pairCode W { head := 0, cells := t.cells }) + = symCode (t.cells 1) := by + rw [verdictSym, drop_pairCode_rewound W t, + show W + 1 = 1 + 1 + (W - 1) from by omega, cellsCode_add, cellsCode_add] + simp only [cellsCode_one, List.append_assoc] + rw [List.drop_left' (by rw [symCode_length]), List.take_left' (by rw [symCode_length])] + +/-! ## The test -/ + +/-- Is the record an accepting halting configuration? -/ +def acceptFlag (qcode R ruler u : List Bool) : List Bool := + andBit (eqFlag ((blockAt R u 0).take qcode.length) qcode) + (eqFlag (verdictSym R (rewindCode R ruler (outPair R u))) (symCode Γ.one)) + +theorem acceptFlag_flag (qcode R ruler u : List Bool) : + acceptFlag qcode R ruler u = [true] ∨ acceptFlag qcode R ruler u = [false] := by + rw [acceptFlag] + rcases eqFlag_flag ((blockAt R u 0).take qcode.length) qcode with h | h <;> + rcases eqFlag_flag (verdictSym R (rewindCode R ruler (outPair R u))) + (symCode Γ.one) with h' | h' <;> rw [h, h'] <;> simp [andBit] + +/-! ## The test is polynomial-time -/ + +theorem acceptFlagFn_mem_FP (qcode : List Bool) {Rf rulerf uf : List Bool → List Bool} + (hR : Rf ∈ FP) (hruler : rulerf ∈ FP) (hu : uf ∈ FP) : + (fun w => acceptFlag qcode (Rf w) (rulerf w) (uf w)) ∈ FP := by + have hqc : (fun _ : List Bool => qcode) ∈ FP := constFn_mem_FP qcode + have hstate : (fun w => (blockAt (Rf w) (uf w) 0).take qcode.length) ∈ FP := by + refine mem_FP_of_eq (Cobham.takeLenFn_mem_FP hqc (blockAtFn_mem_FP hR hu 0)) fun w => rfl + have hout : (fun w => outPair (Rf w) (uf w)) ∈ FP := outPairFn_mem_FP hR hu + have hlen : ∀ w, (outPair (Rf w) (uf w)).length ≤ 2 * (Rf w).length := by + intro w + rw [outPair, List.length_append, blockAt, blockAt] + have h1 := List.length_take_le (Rf w).length ((uf w).drop (3 * (Rf w).length)) + have h2 := List.length_take_le (Rf w).length ((uf w).drop (4 * (Rf w).length)) + omega + have hrew : (fun w => rewindCode (Rf w) (rulerf w) (outPair (Rf w) (uf w))) ∈ FP := + rewindCodeFn_mem_FP hR hruler hout hlen + have hverd : (fun w => verdictSym (Rf w) + (rewindCode (Rf w) (rulerf w) (outPair (Rf w) (uf w)))) ∈ FP := by + refine mem_FP_of_eq (Cobham.takeLenFn_mem_FP (constFn_mem_FP [false, false]) + (dropLenFn_mem_FP (constFn_mem_FP [false, false]) (dropLenFn_mem_FP hR hrew))) + fun w => rfl + exact andBitFn_mem_FP (eqFlagFn_mem_FP hstate hqc) + (eqFlagFn_mem_FP hverd (constFn_mem_FP (symCode Γ.one))) + +end Complexity diff --git a/Complexitylib/Classes/P/Cobham/Internal/CodeRewind.lean b/Complexitylib/Classes/P/Cobham/Internal/CodeRewind.lean new file mode 100644 index 00000000..fbe5cb20 --- /dev/null +++ b/Complexitylib/Classes/P/Cobham/Internal/CodeRewind.lean @@ -0,0 +1,141 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.CodeStep + +/-! +# Reading the verdict cell out of a record + +⚠️ Unreviewed by Bolton + +Acceptance is a property of cell `1` of the output tape, but a code stores each +tape split at its head, so where cell `1` sits depends on the head. Driving the +head back to cell `0` first puts it in a fixed place: the encoding's right +half-block is then the whole tape in order, two bits per cell, so cell `1` is +bits `2` and `3`. + +That is the same rewind the completeness direction of Cobham's theorem uses to +read a simulated machine's output, `Cobham.rewindFn`, run here as a loop of its +own. + +## Main definitions + +- `Complexity.outPair` — the output tape's two blocks, read out of a record +- `Complexity.rewindStepP` — one rewind step, on the packed state +- `Complexity.rewindCode` — a whole rewind + +## Main results + +- `Complexity.outPair_cfgCode` — the two blocks are the output tape's code +- `Complexity.rewindCode_pairCode` — a long enough rewind parks the head +- `Complexity.rewindStepP_mem_FP`, `Complexity.rewindCodeFn_mem_FP` — both are + polynomial-time +-/ + +@[expose] public section + +namespace Complexity + +open Cobham + +variable {k : ℕ} + +/-! ## The output tape inside a record -/ + +/-- Block `3` is the output tape's left half. -/ +theorem blockAt_cfgCode_outputLeft {Q : Type} [Fintype Q] [DecidableEq Q] (W : ℕ) + (c : Cfg k Q) : + blockAt (blockRuler W) (Cobham.cfgCode W c) 3 + = padTo (blockRuler W) (leftCode c.output) := by + rw [blockAt_cfgCode W c 3 (by rw [cfgBlocks_length]; omega)] + rfl + +/-- Block `4` is the output tape's right half. -/ +theorem blockAt_cfgCode_outputRight {Q : Type} [Fintype Q] [DecidableEq Q] (W : ℕ) + (c : Cfg k Q) : + blockAt (blockRuler W) (Cobham.cfgCode W c) 4 + = padTo (blockRuler W) (rightCode c.output W) := by + rw [blockAt_cfgCode W c 4 (by rw [cfgBlocks_length]; omega)] + rfl + +/-- The output tape's code, read out of a record. -/ +def outPair (R u : List Bool) : List Bool := blockAt R u 3 ++ blockAt R u 4 + +theorem outPairFn_mem_FP {a b : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) : + (fun z => outPair (a z) (b z)) ∈ FP := + Cobham.appendFn_mem_FP (blockAtFn_mem_FP ha hb 3) (blockAtFn_mem_FP ha hb 4) + +/-- **The two blocks are the output tape's code.** -/ +theorem outPair_cfgCode {Q : Type} [Fintype Q] [DecidableEq Q] (W : ℕ) (c : Cfg k Q) : + outPair (blockRuler W) (Cobham.cfgCode W c) = pairCode W c.output := by + rw [outPair, blockAt_cfgCode_outputLeft, blockAt_cfgCode_outputRight, pairCode] + +/-! ## The rewind loop -/ + +/-- One rewind step, on the packed state `pair R z`. -/ +def rewindStepP (z : List Bool) : List Bool := + pair (pairFst z) (rewindFn (pairFst z) (pairSnd z)) + +theorem rewindStepP_pack (R z : List Bool) : + rewindStepP (pair R z) = pair R (rewindFn R z) := by + rw [rewindStepP, pairFst_pair, pairSnd_pair] + +theorem rewindStepP_iterate (R z : List Bool) (n : ℕ) : + rewindStepP^[n] (pair R z) = pair R ((rewindFn R)^[n] z) := by + induction n generalizing z with + | zero => rfl + | succ n ih => + rw [Function.iterate_succ_apply, rewindStepP_pack, ih, Function.iterate_succ_apply] + +/-- A whole rewind: one step per bit of the ruler. -/ +def rewindCode (R ruler z : List Bool) : List Bool := + pairSnd (rewindStepP^[ruler.length] (pair R z)) + +theorem rewindCode_eq (R ruler z : List Bool) : + rewindCode R ruler z = (rewindFn R)^[ruler.length] z := by + rw [rewindCode, rewindStepP_iterate, pairSnd_pair] + +/-- **A long enough rewind parks the head at cell `0`.** -/ +theorem rewindCode_pairCode (W : ℕ) (t : Tape) (hinv : t.StartInvariant) + (hW : t.head ≤ W) (ruler : List Bool) (hlen : W ≤ ruler.length) : + rewindCode (blockRuler W) ruler (pairCode W t) + = pairCode W { head := 0, cells := t.cells } := by + rw [rewindCode_eq, iterate_rewindFn t hinv hW ruler.length, + rewound t (le_trans hW hlen)] + +/-! ## Both are polynomial-time -/ + +theorem rewindFnFn_mem_FP {a b : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) : + (fun z => rewindFn (a z) (b z)) ∈ FP := + binFn_mem_FP (g := rewindFn) (Cobham.rewindFn_mem (Cobham.proj 0) (Cobham.proj 1)) ha hb + +theorem rewindStepP_mem_FP : rewindStepP ∈ FP := by + have hid : (fun z : List Bool => z) ∈ FP := CobhamFP_subset_FP (Cobham.proj 0) + have hfst : (fun z => pairFst z) ∈ FP := Cobham.fstBlock_mem_FP + have hsnd : (fun z => pairSnd z) ∈ FP := Cobham.sndBlock_mem_FP + exact Cobham.pairFn_mem_FP hfst (rewindFnFn_mem_FP hfst hsnd) + +/-- **The rewind is polynomial-time.** -/ +theorem rewindCodeFn_mem_FP {Rf rulerf zf : List Bool → List Bool} (hR : Rf ∈ FP) + (hruler : rulerf ∈ FP) (hz : zf ∈ FP) + (hlen : ∀ w, (zf w).length ≤ 2 * (Rf w).length) : + (fun w => rewindCode (Rf w) (rulerf w) (zf w)) ∈ FP := by + have hinit : (fun w => pair (Rf w) (zf w)) ∈ FP := Cobham.pairFn_mem_FP hR hz + have hwidth : (fun w => pair (Rf w) (wideRuler 2 (Rf w))) ∈ FP := + Cobham.pairFn_mem_FP hR (wideRulerFn_mem_FP hR 2) + have hbound : ∀ w, ∀ n ≤ (rulerf w).length, + (rewindStepP^[n] (pair (Rf w) (zf w))).length + ≤ (pair (Rf w) (wideRuler 2 (Rf w))).length := by + intro w n _ + rw [rewindStepP_iterate, pair_length, pair_length, wideRuler_length] + have := Cobham.iterate_rewindFn_length_le (Rf w) (zf w) (hlen w) n + omega + have h := Cobham.iterate_mem_FP rewindStepP_mem_FP hinit hruler hwidth hbound + have h2 := mem_FP_comp h Cobham.sndBlock_mem_FP + refine mem_FP_of_eq h2 fun w => ?_ + rw [Function.comp_apply, rewindCode] + +end Complexity diff --git a/Complexitylib/Classes/P/Cobham/Internal/CodeStep.lean b/Complexitylib/Classes/P/Cobham/Internal/CodeStep.lean new file mode 100644 index 00000000..b6023ba3 --- /dev/null +++ b/Complexitylib/Classes/P/Cobham/Internal/CodeStep.lean @@ -0,0 +1,66 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.PolyRuler + +/-! +# What the encoded step does to a reachable configuration + +⚠️ Unreviewed by Bolton + +`Cobham.stepFn` tracks a machine's step only on configurations that respect the +encoding's window: every head inside it, every tape carrying its left-end +marker. This file collects those side conditions into `Complexity.CodeInv`, +shows that every configuration of a space-bounded machine's configuration graph +satisfies it, and reads off what `Complexity.nstepFn` computes there. + +## Main definitions + +- `Complexity.CodeInv` — the encoding's side conditions on a configuration + +## Main results + +- `Complexity.cfgCode_length` — a code is exactly `2(k+2)+1` blocks wide +-/ + +@[expose] public section + +namespace Complexity + +open Cobham + +variable {k : ℕ} + +/-! ## The width of a code -/ + +/-- The number of blocks in a code: one for the state and two per tape. -/ +def codeBlocks (k : ℕ) : ℕ := 2 * (k + 2) + 1 + +private theorem length_flatten_const {α : Type} (bs : List (List α)) (w : ℕ) + (h : ∀ b ∈ bs, b.length = w) : bs.flatten.length = bs.length * w := by + induction bs with + | nil => simp + | cons b bs ih => + rw [List.flatten_cons, List.length_append, h b (List.mem_cons_self), + ih (fun c hc => h c (List.mem_cons_of_mem _ hc)), List.length_cons, Nat.succ_mul] + omega + +/-- **A code is exactly `2(k+2)+1` blocks wide.** -/ +@[simp] theorem cfgCode_length {Q : Type} [Fintype Q] [DecidableEq Q] (W : ℕ) + (c : Cfg k Q) : + (Cobham.cfgCode W c).length = codeBlocks k * (blockRuler W).length := by + rw [Cobham.cfgCode, length_flatten_const _ _ (cfgBlocks_width W c), cfgBlocks_length, codeBlocks] + +/-! ## The window conditions -/ + +/-- The side conditions under which the encoded step tracks the real one. -/ +structure CodeInv {Q : Type} (W : ℕ) (c : Cfg k Q) : Prop where + /-- Every tape carries its left-end marker. -/ + start : ∀ t ∈ cfgTapes c, t.StartInvariant + /-- Every head is inside the encoded window. -/ + head : ∀ t ∈ cfgTapes c, t.head ≤ W + +end Complexity diff --git a/Complexitylib/Classes/P/Cobham/Internal/FPBridge.lean b/Complexitylib/Classes/P/Cobham/Internal/FPBridge.lean new file mode 100644 index 00000000..f954ccb2 --- /dev/null +++ b/Complexitylib/Classes/P/Cobham/Internal/FPBridge.lean @@ -0,0 +1,141 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham +public import Complexitylib.Classes.P.Composition +public import Complexitylib.Classes.P.Cobham.Internal +public import Complexitylib.Classes.P.Cobham.Internal.StringOps + +/-! +# Programming with polynomial-time string functions + +⚠️ Unreviewed by Bolton + +Cobham's theorem makes `FP` a *programming language*: a function is +polynomial-time exactly when it belongs to the algebra, so a construction can be +written as a composition of small pieces instead of assembled as a machine. This +file collects the glue that makes the two levels interoperate. + +The algebra is stated at every arity, over vectors of arguments, while `FP` is +unary; the bridge is the pairing. `binFn_mem_FP` turns a two-argument member of +the algebra into an `FP` closure rule, and the rest of the file is that rule +applied to the toolkit of +`Complexitylib.Classes.P.Cobham.Internal.StringOps` and +`Complexitylib.Classes.P.Cobham.Internal.Algebra`. + +## Main results + +- `Cobham.fstBlockFn`, `Cobham.sndBlockFn` — the pair decoders, in the algebra +- `unFn_mem_FP`, `binFn_mem_FP` — algebra members become `FP` closure rules +- `constFn_mem_FP` — every constant is polynomial-time +- `mem_FP_of_eq` — `FP` respects pointwise equality +-/ + +@[expose] public section + +namespace Complexity + +namespace Cobham + +/-- The first pair decoder is in the algebra, being polynomial-time. -/ +theorem fstBlockFn {n : ℕ} {g : (Fin n → List Bool) → List Bool} (hg : Cobham g) : + Cobham fun v : Fin n → List Bool => pairFst (g v) := + (Cobham.comp (FP_subset_CobhamFP fstBlock_mem_FP) fun _ : Fin 1 => hg).of_eq fun _ => rfl + +/-- The second pair decoder is in the algebra. -/ +theorem sndBlockFn {n : ℕ} {g : (Fin n → List Bool) → List Bool} (hg : Cobham g) : + Cobham fun v : Fin n → List Bool => pairSnd (g v) := + (Cobham.comp (FP_subset_CobhamFP sndBlock_mem_FP) fun _ : Fin 1 => hg).of_eq fun _ => rfl + +end Cobham + +/-- `FP` respects pointwise equality of functions — the counterpart of +`Cobham.of_eq`, needed because these closure rules produce syntactically +specific lambda terms. -/ +theorem mem_FP_of_eq {f g : List Bool → List Bool} (hf : f ∈ FP) (h : ∀ z, f z = g z) : + g ∈ FP := by + have hfg : f = g := funext h + rwa [hfg] at hf + +/-! ## From the algebra to `FP` closure rules -/ + +/-- **A one-argument member of the algebra is an `FP` closure rule.** -/ +theorem unFn_mem_FP {g : List Bool → List Bool} + (hg : Cobham fun v : Fin 1 → List Bool => g (v 0)) + {a : List Bool → List Bool} (ha : a ∈ FP) : (fun z => g (a z)) ∈ FP := by + have hg' : g ∈ FP := CobhamFP_subset_FP hg + have h := mem_FP_comp ha hg' + simpa [Function.comp] using h + +/-- **A two-argument member of the algebra is an `FP` closure rule.** The two +levels differ only in how arguments are presented: the algebra takes a vector, +`FP` takes the pairing of the two values. -/ +theorem binFn_mem_FP {g : List Bool → List Bool → List Bool} + (hg : Cobham fun v : Fin 2 → List Bool => g (v 0) (v 1)) + {a b : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) : + (fun z => g (a z) (b z)) ∈ FP := by + have hpacked : Cobham fun v : Fin 1 → List Bool => + g (pairFst (v 0)) (pairSnd (v 0)) := + (Cobham.comp₂ hg (Cobham.fstBlockFn (Cobham.proj 0)) + (Cobham.sndBlockFn (Cobham.proj 0))).of_eq fun _ => rfl + have hfp : (fun w => g (pairFst w) (pairSnd w)) ∈ FP := + CobhamFP_subset_FP hpacked + have h := mem_FP_comp (Cobham.pairFn_mem_FP ha hb) hfp + have heq : ((fun w => g (pairFst w) (pairSnd w)) ∘ fun z => pair (a z) (b z)) + = fun z => g (a z) (b z) := by + funext z + simp [Function.comp] + rwa [heq] at h + +/-! ## The rules -/ + +/-- Dropping a prefix at another value's width is polynomial-time. -/ +theorem dropLenFn_mem_FP {a b : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) : + (fun z => (b z).drop (a z).length) ∈ FP := + binFn_mem_FP (g := fun p q => q.drop p.length) + (Cobham.dropFn (Cobham.proj 0) (Cobham.proj 1)) ha hb + +/-- Conjunction of flags is polynomial-time. -/ +theorem andBitFn_mem_FP {a b : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) : + (fun z => andBit (a z) (b z)) ∈ FP := + binFn_mem_FP (g := andBit) (Cobham.andFn (Cobham.proj 0) (Cobham.proj 1)) ha hb + +/-- Negation of a flag is polynomial-time. -/ +theorem notBitFn_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => notBit (a z)) ∈ FP := + unFn_mem_FP (g := notBit) (Cobham.notFn (Cobham.proj 0)) ha + +/-- The length comparison is polynomial-time. -/ +theorem lenLeFlagFn_mem_FP {a b : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) : + (fun z => Cobham.lenLeFlag (a z) (b z)) ∈ FP := + binFn_mem_FP (g := Cobham.lenLeFlag) + (Cobham.lenLeFlag_mem (Cobham.proj 0) (Cobham.proj 1)) ha hb + +/-- The equality test is polynomial-time. -/ +theorem eqFlagFn_mem_FP {a b : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) : + (fun z => Cobham.eqFlag (a z) (b z)) ∈ FP := + binFn_mem_FP (g := Cobham.eqFlag) + (Cobham.eqFlag_mem (Cobham.proj 0) (Cobham.proj 1)) ha hb + +/-- Reading a fixed field of a block-aligned string is polynomial-time. -/ +theorem blockAtFn_mem_FP {a b : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) + (i : ℕ) : (fun z => blockAt (a z) (b z) i) ∈ FP := + binFn_mem_FP (g := fun p q => blockAt p q i) + (Cobham.blockFn (Cobham.proj 0) (Cobham.proj 1) i) ha hb + +/-! ## Decoding a pair component of a polynomial-time value -/ + +theorem fstBlockOf_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => pairFst (a z)) ∈ FP := by + have := mem_FP_comp ha Cobham.fstBlock_mem_FP + simpa [Function.comp] using this + +theorem sndBlockOf_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => pairSnd (a z)) ∈ FP := by + have := mem_FP_comp ha Cobham.sndBlock_mem_FP + simpa [Function.comp] using this + +end Complexity diff --git a/Complexitylib/Classes/P/Cobham/Internal/HeadOps.lean b/Complexitylib/Classes/P/Cobham/Internal/HeadOps.lean new file mode 100644 index 00000000..dd985e2c --- /dev/null +++ b/Complexitylib/Classes/P/Cobham/Internal/HeadOps.lean @@ -0,0 +1,52 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge + +/-! +# Head-of-list operations in polynomial time + +Three small string operations the `FP` toolkit uses constantly: reading the +head symbol (`Cobham.selectHead`), testing for the empty string (`emptyFlag`) +and dropping the first symbol (`dropOne`), each with its evaluation lemmas +and its `FP` membership. +-/ + +@[expose] public section + +namespace Complexity +open Cobham + +@[simp] theorem selectHead_cons_true (x y : List Bool) : + Cobham.selectHead [true] x y = x := by + rw [Cobham.selectHead]; simp + +@[simp] theorem selectHead_cons_false (x y : List Bool) : + Cobham.selectHead [false] x y = y := by + rw [Cobham.selectHead]; simp + +/-- Is the string empty, as a flag. -/ +def emptyFlag (y : List Bool) : List Bool := lenLeFlag [] y + +@[simp] theorem emptyFlag_nil : emptyFlag [] = [true] := rfl + +theorem emptyFlag_cons (b : Bool) (y : List Bool) : emptyFlag (b :: y) = [false] := by + rw [emptyFlag, lenLeFlag] + simp [nonemptyFlag, notBit] + +theorem emptyFlagFn_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => emptyFlag (a z)) ∈ FP := + lenLeFlagFn_mem_FP (constFn_mem_FP []) ha + +/-- Drop the leading bit. -/ +def dropOne (y : List Bool) : List Bool := y.drop 1 + +theorem dropOneFn_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => dropOne (a z)) ∈ FP := by + have := dropLenFn_mem_FP (constFn_mem_FP [false]) ha + simpa [dropOne] using this + +end Complexity diff --git a/Complexitylib/Classes/P/Cobham/Internal/Iterate.lean b/Complexitylib/Classes/P/Cobham/Internal/Iterate.lean index 64b10f1a..f6b8dfc2 100644 --- a/Complexitylib/Classes/P/Cobham/Internal/Iterate.lean +++ b/Complexitylib/Classes/P/Cobham/Internal/Iterate.lean @@ -876,13 +876,9 @@ theorem iterTM_computesInTime (M : TM k) {G : List Bool → List Bool} {tp : Pol `Complexity.iterBound` is a sum of products of polynomial evaluations, so the closure API of `Complexitylib.Asymptotics.PolyBound` bounds it directly. -/ -theorem polyBound_iterBound (k : ℕ) (tp p r : Polynomial ℕ) : - PolyBound (iterBound k tp p r) := by - have hcomp : PolyBound (fun n => tp.eval (r.eval n)) := - PolyBound.mono (PolyBound.eval (tp.comp r)) - (fun n => le_of_eq (by rw [Polynomial.eval_comp])) +/-- `Complexity.setupBound` is polynomially bounded. -/ +theorem polyBound_setupBound (p : Polynomial ℕ) : PolyBound (setupBound p) := by have hp : PolyBound (fun n => p.eval n) := PolyBound.eval p - have hr : PolyBound (fun n => r.eval n) := PolyBound.eval r have hpow : PolyBound (fun n => (n + 1) ^ (polyCoeffs p).length) := PolyBound.pow (PolyBound.add PolyBound.id (PolyBound.const 1)) _ have hM : PolyBound (fun n => polyM p n) := by @@ -899,42 +895,54 @@ theorem polyBound_iterBound (k : ℕ) (tp p r : Polynomial ℕ) : rw [show (fun n => layerBudget (polyM p n)) = fun n => 4 * opBudget (polyM p n) + 3 from rfl] exact PolyBound.add (PolyBound.mul (PolyBound.const _) hop) (PolyBound.const _) - have hsetup : PolyBound (setupBound p) := by - rw [show setupBound p = fun n => 1 + 1 + (2 * n + 4) + 1 + - (opBudget (polyM p n) + 1 + - ((p.natDegree + 1) * (layerBudget (polyM p n) + 1) + 1)) + 1 + (n + 3) from rfl] - exact PolyBound.add (PolyBound.add (PolyBound.add (PolyBound.add (PolyBound.add - (PolyBound.add (PolyBound.const _) (PolyBound.const _)) - (PolyBound.add (PolyBound.mul (PolyBound.const 2) PolyBound.id) (PolyBound.const _))) - (PolyBound.const _)) - (PolyBound.add (PolyBound.add hop (PolyBound.const _)) - (PolyBound.add - (PolyBound.mul (PolyBound.const _) (PolyBound.add hlayer (PolyBound.const _))) - (PolyBound.const _)))) - (PolyBound.const _)) (PolyBound.add PolyBound.id (PolyBound.const _)) + rw [show setupBound p = fun n => 1 + 1 + (2 * n + 4) + 1 + + (opBudget (polyM p n) + 1 + + ((p.natDegree + 1) * (layerBudget (polyM p n) + 1) + 1)) + 1 + (n + 3) from rfl] + exact PolyBound.add (PolyBound.add (PolyBound.add (PolyBound.add (PolyBound.add + (PolyBound.add (PolyBound.const _) (PolyBound.const _)) + (PolyBound.add (PolyBound.mul (PolyBound.const 2) PolyBound.id) (PolyBound.const _))) + (PolyBound.const _)) + (PolyBound.add (PolyBound.add hop (PolyBound.const _)) + (PolyBound.add + (PolyBound.mul (PolyBound.const _) (PolyBound.add hlayer (PolyBound.const _))) + (PolyBound.const _)))) + (PolyBound.const _)) (PolyBound.add PolyBound.id (PolyBound.const _)) + +/-- `Complexity.tailBound` is polynomially bounded in the input length, for any +polynomially bounded result length. -/ +theorem polyBound_tailBound (k : ℕ) (p : Polynomial ℕ) (m : ℕ → ℕ) (hm : PolyBound m) : + PolyBound (fun n => tailBound k (p.eval n) (m n)) := by + have hp : PolyBound (fun n => p.eval n) := PolyBound.eval p + rw [show (fun n => tailBound k (p.eval n) (m n)) = fun n => + 1 + 1 + (p.eval n + 1 + 2) + 1 + + ((k + 1) * (p.eval n + 4) + p.eval n * 4 + 8 + 1 + ((k + 1) * (p.eval n + 4) + 1)) + 1 + + (2 * m n + 5 + 1 + + (1 * (p.eval n + 4) + p.eval n * 4 + 8 + 1 + (1 * (p.eval n + 4) + 1))) from rfl] + have hbase : PolyBound (fun n => p.eval n + 4) := PolyBound.add hp (PolyBound.const _) + have hk : PolyBound (fun n => (k + 1) * (p.eval n + 4)) := + PolyBound.mul (PolyBound.const _) hbase + have h1 : PolyBound (fun n => 1 * (p.eval n + 4)) := PolyBound.mul (PolyBound.const _) hbase + have h4 : PolyBound (fun n => p.eval n * 4) := PolyBound.mul hp (PolyBound.const _) + exact PolyBound.add (PolyBound.add (PolyBound.add (PolyBound.add (PolyBound.add + (PolyBound.add (PolyBound.const _) (PolyBound.const _)) + (PolyBound.add (PolyBound.add hp (PolyBound.const _)) (PolyBound.const _))) + (PolyBound.const _)) + (PolyBound.add (PolyBound.add (PolyBound.add (PolyBound.add hk h4) (PolyBound.const _)) + (PolyBound.const _)) (PolyBound.add hk (PolyBound.const _)))) (PolyBound.const _)) + (PolyBound.add (PolyBound.add (PolyBound.add (PolyBound.mul (PolyBound.const 2) hm) + (PolyBound.const _)) (PolyBound.const _)) + (PolyBound.add (PolyBound.add (PolyBound.add (PolyBound.add h1 h4) (PolyBound.const _)) + (PolyBound.const _)) (PolyBound.add h1 (PolyBound.const _)))) + +theorem polyBound_iterBound (k : ℕ) (tp p r : Polynomial ℕ) : + PolyBound (iterBound k tp p r) := by + have hcomp : PolyBound (fun n => tp.eval (r.eval n)) := + PolyBound.mono (PolyBound.eval (tp.comp r)) + (fun n => le_of_eq (by rw [Polynomial.eval_comp])) + have hr : PolyBound (fun n => r.eval n) := PolyBound.eval r + have hsetup : PolyBound (setupBound p) := polyBound_setupBound p have htail : ∀ m : ℕ → ℕ, PolyBound m → - PolyBound (fun n => tailBound k (p.eval n) (m n)) := by - intro m hm - rw [show (fun n => tailBound k (p.eval n) (m n)) = fun n => - 1 + 1 + (p.eval n + 1 + 2) + 1 + - ((k + 1) * (p.eval n + 4) + p.eval n * 4 + 8 + 1 + ((k + 1) * (p.eval n + 4) + 1)) + 1 + - (2 * m n + 5 + 1 + - (1 * (p.eval n + 4) + p.eval n * 4 + 8 + 1 + (1 * (p.eval n + 4) + 1))) from rfl] - have hbase : PolyBound (fun n => p.eval n + 4) := PolyBound.add hp (PolyBound.const _) - have hk : PolyBound (fun n => (k + 1) * (p.eval n + 4)) := - PolyBound.mul (PolyBound.const _) hbase - have h1 : PolyBound (fun n => 1 * (p.eval n + 4)) := PolyBound.mul (PolyBound.const _) hbase - have h4 : PolyBound (fun n => p.eval n * 4) := PolyBound.mul hp (PolyBound.const _) - exact PolyBound.add (PolyBound.add (PolyBound.add (PolyBound.add (PolyBound.add - (PolyBound.add (PolyBound.const _) (PolyBound.const _)) - (PolyBound.add (PolyBound.add hp (PolyBound.const _)) (PolyBound.const _))) - (PolyBound.const _)) - (PolyBound.add (PolyBound.add (PolyBound.add (PolyBound.add hk h4) (PolyBound.const _)) - (PolyBound.const _)) (PolyBound.add hk (PolyBound.const _)))) (PolyBound.const _)) - (PolyBound.add (PolyBound.add (PolyBound.add (PolyBound.mul (PolyBound.const 2) hm) - (PolyBound.const _)) (PolyBound.const _)) - (PolyBound.add (PolyBound.add (PolyBound.add (PolyBound.add h1 h4) (PolyBound.const _)) - (PolyBound.const _)) (PolyBound.add h1 (PolyBound.const _)))) + PolyBound (fun n => tailBound k (p.eval n) (m n)) := polyBound_tailBound k p rw [show iterBound k tp p r = fun n => setupBound p n + 1 + (tailBound k (p.eval n) (n + 2) + 1 + (n * (tp.eval (r.eval n) + 1 + tailBound k (p.eval n) (r.eval n) + 2) + (n + 2)) + 1 + diff --git a/Complexitylib/Classes/P/Cobham/Internal/PVerdict.lean b/Complexitylib/Classes/P/Cobham/Internal/PVerdict.lean new file mode 100644 index 00000000..9a36bbd8 --- /dev/null +++ b/Complexitylib/Classes/P/Cobham/Internal/PVerdict.lean @@ -0,0 +1,373 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.PolyRuler +public import Complexitylib.Classes.P.Cobham.Internal.CodeAccept +public import Complexitylib.Classes.P.NormalForm +public import Complexitylib.Classes.P.DecisionFn + +/-! +# A language in `P` has a polynomial-time verdict function + +⚠️ Unreviewed by Bolton + +`Complexity.mem_P_of_decisionFn` puts a language in `P` given a verdict function in `FP`. This +file is the converse: a language in `P` *has* such a function. That is what a development needs +whenever a polynomial-time predicate has to be consulted from inside another polynomial-time +computation — the predicate arrives as a machine, and only a function can be composed. + +The function is the machine's own run, carried out inside the algebra: `Cobham.initFn` encodes +the initial configuration, `Cobham.stepFn` advances it, and after the machine's time bound many +steps `Complexity.acceptFlag` reads the verdict cell off the resulting code. Halted +configurations are fixed points of the encoded step, so running for exactly the time bound is +safe however early the machine stops. + +## Main definitions + +- `Complexity.TM.stepOrStay`, `Complexity.TM.runTo` — the run as a total iteration +- `Complexity.codeStep` — the encoded step with its ruler carried alongside +- `Complexity.pVerdict` — the verdict function + +## Main results + +- `Complexity.runCode_eq` — the encoded run is the code of the real one +- `Complexity.pVerdict_eq_true_iff` — the verdict function decides the language +- `Complexity.pVerdict_mem_FP` — and it is polynomial-time +- `Complexity.exists_decisionFn_of_mem_P` — hence every language in `P` has one +-/ + +@[expose] public section + +namespace Complexity + +open Cobham + +variable {k : ℕ} + +/-! ## The run as a total iteration -/ + +namespace TM + +/-- One step of a deterministic machine, staying put once it has halted. -/ +def stepOrStay (tm : TM k) (c : Cfg k tm.Q) : Cfg k tm.Q := (tm.step c).getD c + +theorem stepOrStay_of_halted (tm : TM k) {c : Cfg k tm.Q} (h : c.state = tm.qhalt) : + tm.stepOrStay c = c := by + rw [stepOrStay, TM.step, if_pos h] + rfl + +theorem stepOrStay_of_step (tm : TM k) {c c' : Cfg k tm.Q} (h : tm.step c = some c') : + tm.stepOrStay c = c' := by + rw [stepOrStay, h] + rfl + +/-- The configuration after `n` steps, halted ones counting as no-ops. -/ +def runTo (tm : TM k) (x : List Bool) (n : ℕ) : Cfg k tm.Q := + (tm.stepOrStay)^[n] (tm.initCfg x) + +@[simp] theorem runTo_zero (tm : TM k) (x : List Bool) : tm.runTo x 0 = tm.initCfg x := rfl + +theorem runTo_succ (tm : TM k) (x : List Bool) (n : ℕ) : + tm.runTo x (n + 1) = tm.stepOrStay (tm.runTo x n) := by + rw [runTo, runTo, Function.iterate_succ_apply'] + +/-- A bounded walk is exactly that many total steps. -/ +theorem iterate_stepOrStay_of_reachesIn (tm : TM k) : + ∀ {t : ℕ} {c c' : Cfg k tm.Q}, tm.reachesIn t c c' → (tm.stepOrStay)^[t] c = c' := by + intro t c c' h + induction h with + | zero => rfl + | step hstep _ ih => + rw [Function.iterate_succ_apply, stepOrStay_of_step tm hstep] + exact ih + +/-- A halted configuration is a fixed point of the total step. -/ +theorem iterate_stepOrStay_halted (tm : TM k) {c : Cfg k tm.Q} (h : c.state = tm.qhalt) : + ∀ n, (tm.stepOrStay)^[n] c = c := by + intro n + induction n with + | zero => rfl + | succ n ih => rw [Function.iterate_succ_apply, stepOrStay_of_halted tm h, ih] + +/-- Once the machine has halted, the run stays where it stopped. -/ +theorem runTo_of_halted (tm : TM k) (x : List Bool) {t : ℕ} {c : Cfg k tm.Q} + (h : tm.reachesIn t (tm.initCfg x) c) (hh : c.state = tm.qhalt) {n : ℕ} (hn : t ≤ n) : + tm.runTo x n = c := by + have ht : (tm.stepOrStay)^[t] (tm.initCfg x) = c := iterate_stepOrStay_of_reachesIn tm h + rw [runTo, show n = n - t + t from by omega, Function.iterate_add_apply, ht, + iterate_stepOrStay_halted tm hh] + +/-- Every point of the run is reached by a walk no longer than the index. -/ +theorem exists_reachesIn_runTo (tm : TM k) (x : List Bool) : + ∀ n, ∃ t ≤ n, tm.reachesIn t (tm.initCfg x) (tm.runTo x n) := by + intro n + induction n with + | zero => exact ⟨0, le_rfl, TM.reachesIn.zero⟩ + | succ n ih => + obtain ⟨t, ht, hr⟩ := ih + rw [runTo_succ] + rcases hs : tm.step (tm.runTo x n) with _ | c' + · refine ⟨t, by omega, ?_⟩ + rw [stepOrStay, hs] + exact hr + · refine ⟨t + 1, by omega, ?_⟩ + rw [stepOrStay_of_step tm hs] + exact tm.reachesIn_trans hr (TM.reachesIn.step hs TM.reachesIn.zero) + +/-- The left-end markers survive any walk. -/ +theorem startInvariant_of_reachesIn (tm : TM k) : + ∀ {t : ℕ} {c c' : Cfg k tm.Q}, tm.reachesIn t c c' → + c.input.StartInvariant → (∀ i, (c.work i).StartInvariant) → c.output.StartInvariant → + c'.input.StartInvariant ∧ (∀ i, (c'.work i).StartInvariant) ∧ + c'.output.StartInvariant := by + intro t c c' h + induction h with + | zero => exact fun a b c => ⟨a, b, c⟩ + | step hstep _ ih => + intro hi hw ho + obtain ⟨hi', hw', ho'⟩ := Tape.StartInvariant.step _ hstep hi hw ho + exact ih hi' hw' ho' + +/-- Every point of the run is inside the window a time bound gives. -/ +theorem codeInv_runTo (tm : TM k) (x : List Bool) {W n : ℕ} (hn : n ≤ W) : + CodeInv W (tm.runTo x n) := by + obtain ⟨t, ht, hr⟩ := exists_reachesIn_runTo tm x n + obtain ⟨hi, hw, ho⟩ := startInvariant_of_reachesIn tm hr + (Tape.StartInvariant.init_ofBool x) (fun _ => Tape.StartInvariant.init_nil) + Tape.StartInvariant.init_nil + obtain ⟨hin, hout, hwork⟩ := TM.head_le_of_reachesIn tm hr + refine ⟨fun s hs => ?_, fun s hs => ?_⟩ <;> + · rw [cfgTapes, List.mem_cons, List.mem_cons, List.mem_ofFn] at hs + rcases hs with rfl | rfl | ⟨i, rfl⟩ + · first | exact hi | omega + · first | exact ho | omega + · first | exact hw i | (have := hwork i; omega) + +end TM + +/-! ## The encoded run -/ + +/-- The encoded step, carrying its ruler alongside the code. -/ +noncomputable def codeStep (tm : TM k) (w : List Bool) : List Bool := + pair (pairFst w) (stepFn tm (pairFst w) (pairSnd w)) + +theorem codeStep_pair (tm : TM k) (R z : List Bool) : + codeStep tm (pair R z) = pair R (stepFn tm R z) := by + rw [codeStep, pairFst_pair, pairSnd_pair] + +theorem codeStep_iterate (tm : TM k) (R z : List Bool) : + ∀ n, (codeStep tm)^[n] (pair R z) = pair R ((stepFn tm R)^[n] z) := by + intro n + induction n generalizing z with + | zero => rfl + | succ n ih => + rw [Function.iterate_succ_apply, codeStep_pair, ih, Function.iterate_succ_apply] + +theorem codeStep_mem_FP (tm : TM k) : codeStep tm ∈ FP := by + have hfst : (fun z : List Bool => pairFst z) ∈ FP := Cobham.fstBlock_mem_FP + have hsnd : (fun z : List Bool => pairSnd z) ∈ FP := Cobham.sndBlock_mem_FP + have hstep : (fun w => stepFn tm (pairFst w) (pairSnd w)) ∈ FP := + binFn_mem_FP (g := stepFn tm) + (Cobham.stepFn_mem tm (Cobham.proj 0) (Cobham.proj 1)) hfst hsnd + exact Cobham.pairFn_mem_FP hfst hstep + +/-- **The encoded run is the code of the real one.** -/ +theorem runCode_eq (tm : TM k) (W : ℕ) (hq : Fintype.card tm.Q ≤ blockWidth W) + (x : List Bool) (hx : x.length ≤ W) : + ∀ n ≤ W, (stepFn tm (blockRuler W))^[n] (Cobham.initFn tm (blockRuler W) x) + = Cobham.cfgCode W (tm.runTo x n) := by + intro n + induction n with + | zero => + intro _ + rw [Function.iterate_zero_apply, Cobham.initFn_eq tm W x hx, TM.runTo_zero] + | succ n ih => + intro hn + have hinv : CodeInv W (tm.runTo x n) := TM.codeInv_runTo tm x (by omega) + have hheads : ∀ t ∈ cfgTapes (tm.runTo x n), t.head ≤ W := hinv.head + rw [Function.iterate_succ_apply', ih (by omega), TM.runTo_succ] + rcases hs : tm.step (tm.runTo x n) with _ | c' + · have hhalt : (tm.runTo x n).state = tm.qhalt := by + by_contra hc + simp [TM.step, hc] at hs + rw [TM.stepOrStay, hs] + exact stepFn_halted tm hhalt hq hheads + · rw [TM.stepOrStay_of_step tm hs] + exact stepFn_eq tm hs hq hheads (hinv.start _ (by simp [cfgTapes])) + (fun i => hinv.start _ (by + rw [cfgTapes] + exact List.mem_cons_of_mem _ (List.mem_cons_of_mem _ (List.mem_ofFn.mpr ⟨i, rfl⟩)))) + (stepActs_forall₂ tm _ hinv.start hheads) + +/-! ## Reading the verdict off a code -/ + +/-- **The test decides acceptance**, for a deterministic machine. -/ +theorem acceptFlag_cfgCode_tm (tm : TM k) (W : ℕ) (c : Cfg k tm.Q) + (hq : Fintype.card tm.Q ≤ blockWidth W) (hinv : CodeInv W c) (hW : 1 ≤ W) + (ruler : List Bool) (hruler : W ≤ ruler.length) : + acceptFlag (stateCode tm.qhalt) (blockRuler W) ruler (Cobham.cfgCode W c) = [true] ↔ + c.state = tm.qhalt ∧ c.output.cells 1 = Γ.one := by + have hout : c.output.StartInvariant := hinv.start _ (by simp [cfgTapes]) + have houth : c.output.head ≤ W := hinv.head _ (by simp [cfgTapes]) + have hstate : (blockAt (blockRuler W) (Cobham.cfgCode W c) 0).take + (stateCode tm.qhalt).length = stateCode c.state := by + rw [stateCode_length] + exact state_of_cfgCode W c hq + have hverdict : verdictSym (blockRuler W) + (rewindCode (blockRuler W) ruler (outPair (blockRuler W) (Cobham.cfgCode W c))) + = symCode (c.output.cells 1) := by + rw [outPair_cfgCode, rewindCode_pairCode W c.output hout houth ruler hruler, + verdictSym_rewound W c.output hW] + rw [acceptFlag, andBit_eq_true_iff (eqFlag_flag _ _) (eqFlag_flag _ _), + eqFlag_eq_true_iff, eqFlag_eq_true_iff, hstate, hverdict] + constructor + · rintro ⟨h1, h2⟩ + exact ⟨stateCode_injective h1, symCode_injective h2⟩ + · rintro ⟨h1, h2⟩ + exact ⟨by rw [h1], by rw [h2]⟩ + +/-! ## The verdict function -/ + +/-- The verdict a deterministic machine reaches, computed inside the algebra. -/ +noncomputable def pVerdict (tm : TM k) (wp tp : Polynomial ℕ) (x : List Bool) : List Bool := + acceptFlag (stateCode tm.qhalt) (polyRuler (2 * wp + 2) x) + (wideRuler (codeBlocks k) (polyRuler (2 * wp + 2) x)) + (pairSnd ((codeStep tm)^[(polyRuler tp x).length] + (pair (polyRuler (2 * wp + 2) x) + (Cobham.initFn tm (polyRuler (2 * wp + 2) x) x)))) + +theorem pVerdict_flag (tm : TM k) (wp tp : Polynomial ℕ) (x : List Bool) : + pVerdict tm wp tp x = [true] ∨ pVerdict tm wp tp x = [false] := + acceptFlag_flag _ _ _ _ + +/-- **The verdict function decides the language.** -/ +theorem pVerdict_eq_true_iff (tm : TM k) {L : Language} (wp tp : Polynomial ℕ) + (hdec : tm.DecidesInTime L fun n => tp.eval n) + (hwp : ∀ n, n + tp.eval n + 1 ≤ wp.eval n) + (hq : ∀ n, Fintype.card tm.Q ≤ blockWidth (wp.eval n)) (x : List Bool) : + pVerdict tm wp tp x = [true] ↔ x ∈ L := by + set W := wp.eval x.length with hW + have hbound := hwp x.length + have hxW : x.length ≤ W := by omega + have hW1 : 1 ≤ W := by omega + have hR : polyRuler (2 * wp + 2) x = blockRuler W := (blockRuler_eq_polyRuler wp x).symm + have hruler : W ≤ (wideRuler (codeBlocks k) (blockRuler W)).length := by + rw [wideRuler_length, blockRuler_length, blockWidth] + have hcb : 1 ≤ codeBlocks k := by rw [codeBlocks]; omega + calc W ≤ 1 * (2 * (W + 1)) := by omega + _ ≤ codeBlocks k * (2 * (W + 1)) := Nat.mul_le_mul_right _ hcb + obtain ⟨c, t, htT, hreach, hhalt, hone, hzero⟩ := hdec x + have hrun : tm.runTo x (tp.eval x.length) = c := + TM.runTo_of_halted tm x hreach hhalt (by simpa using htT) + have hcode : pairSnd ((codeStep tm)^[(polyRuler tp x).length] + (pair (polyRuler (2 * wp + 2) x) (Cobham.initFn tm (polyRuler (2 * wp + 2) x) x))) + = Cobham.cfgCode W c := by + rw [codeStep_iterate, pairSnd_pair, polyRuler_length, hR, + runCode_eq tm W (hq x.length) x hxW _ (by omega), hrun] + rw [pVerdict, hcode, hR, + acceptFlag_cfgCode_tm tm W c (hq x.length) + (hrun ▸ TM.codeInv_runTo tm x (n := tp.eval x.length) (by omega)) hW1 _ hruler] + constructor + · rintro ⟨_, hv⟩ + by_contra hx + rw [hzero hx] at hv + exact absurd hv (by decide) + · intro hx + exact ⟨hhalt, hone hx⟩ + +/-! ## The verdict function is polynomial-time -/ + +/-- The encoded run never grows past the code width. -/ +theorem stepFn_iterate_length_le (tm : TM k) (R z : List Bool) + (hz : z.length ≤ codeBlocks k * R.length) : + ∀ n, ((stepFn tm R)^[n] z).length ≤ codeBlocks k * R.length := by + intro n + induction n generalizing z with + | zero => simpa using hz + | succ n ih => + rw [Function.iterate_succ_apply] + exact ih _ (by rw [codeBlocks] at hz ⊢; exact stepFn_length_le tm R z hz) + +/-- The width the whole packed state stays inside. -/ +noncomputable def pStateBound (k : ℕ) (wp : Polynomial ℕ) : Polynomial ℕ := + 2 * (2 * wp + 2) + 2 + Polynomial.C (codeBlocks k) * (2 * wp + 2) + +theorem pStateBound_eval (k : ℕ) (wp : Polynomial ℕ) (n : ℕ) : + (pStateBound k wp).eval n + = 2 * (2 * wp.eval n + 2) + 2 + codeBlocks k * (2 * wp.eval n + 2) := by + simp [pStateBound] + +/-- **The verdict function is polynomial-time.** -/ +theorem pVerdict_mem_FP (tm : TM k) (wp tp : Polynomial ℕ) + (hwp : ∀ n, n ≤ wp.eval n) : pVerdict tm wp tp ∈ FP := by + have hx : (fun x : List Bool => x) ∈ FP := CobhamFP_subset_FP (Cobham.proj 0) + have hRf : (fun x => polyRuler (2 * wp + 2) x) ∈ FP := polyRulerFn_mem_FP _ hx + have hRlen : ∀ x : List Bool, + (polyRuler (2 * wp + 2) x).length = 2 * wp.eval x.length + 2 := by + intro x + rw [polyRuler_length] + simp + have hinit : (fun x => pair (polyRuler (2 * wp + 2) x) + (Cobham.initFn tm (polyRuler (2 * wp + 2) x) x)) ∈ FP := + Cobham.pairFn_mem_FP hRf + (binFn_mem_FP (g := Cobham.initFn tm) + (Cobham.initFn_mem tm (Cobham.proj 0) (Cobham.proj 1)) hRf hx) + have hruler : (fun x => polyRuler tp x) ∈ FP := polyRulerFn_mem_FP _ hx + have hwidth : (fun x => polyRuler (pStateBound k wp) x) ∈ FP := polyRulerFn_mem_FP _ hx + have hbound : ∀ x : List Bool, ∀ n ≤ (polyRuler tp x).length, + ((codeStep tm)^[n] (pair (polyRuler (2 * wp + 2) x) + (Cobham.initFn tm (polyRuler (2 * wp + 2) x) x))).length + ≤ (polyRuler (pStateBound k wp) x).length := by + intro x n _ + have hR : polyRuler (2 * wp + 2) x = blockRuler (wp.eval x.length) := + (blockRuler_eq_polyRuler wp x).symm + have hinitlen : (Cobham.initFn tm (polyRuler (2 * wp + 2) x) x).length + ≤ codeBlocks k * (polyRuler (2 * wp + 2) x).length := by + rw [hR, Cobham.initFn_eq tm _ x (hwp x.length), cfgCode_length] + have := stepFn_iterate_length_le tm (polyRuler (2 * wp + 2) x) _ hinitlen n + rw [codeStep_iterate, pair_length, hRlen x, polyRuler_length, pStateBound_eval] + rw [hRlen x] at this + omega + have hiter := Cobham.iterate_mem_FP (codeStep_mem_FP tm) hinit hruler hwidth hbound + have hcode : (fun x => pairSnd ((codeStep tm)^[(polyRuler tp x).length] + (pair (polyRuler (2 * wp + 2) x) + (Cobham.initFn tm (polyRuler (2 * wp + 2) x) x)))) ∈ FP := by + have := mem_FP_comp hiter Cobham.sndBlock_mem_FP + simpa [Function.comp] using this + exact acceptFlagFn_mem_FP _ hRf (wideRulerFn_mem_FP hRf (codeBlocks k)) hcode + +/-! ## The bridge -/ + +/-- **Every language in `P` has a polynomial-time verdict function.** This is the converse of +`Complexity.mem_P_of_decisionFn_bool`: a polynomial-time predicate can always be consulted from +inside another polynomial-time computation. -/ +theorem exists_decisionFn_of_mem_P {L : Language} (hL : L ∈ P) : + ∃ g : List Bool → Bool, (fun x => [g x]) ∈ FP ∧ ∀ x, x ∈ L ↔ g x = true := by + obtain ⟨k, tm, tp, hdec⟩ := mem_P_iff_decidesInTime_polynomial.mp hL + set wp : Polynomial ℕ := + Polynomial.X + tp + Polynomial.C 1 + Polynomial.C (Fintype.card tm.Q) with hwpdef + have hwe : ∀ n, wp.eval n = n + tp.eval n + 1 + Fintype.card tm.Q := by + intro n + rw [hwpdef] + simp + have hwp : ∀ n, n + tp.eval n + 1 ≤ wp.eval n := by + intro n + rw [hwe] + omega + have hq : ∀ n, Fintype.card tm.Q ≤ blockWidth (wp.eval n) := by + intro n + rw [blockWidth, hwe] + omega + refine ⟨fun x => (pVerdict tm wp tp x).headD false, ?_, fun x => ?_⟩ + · have heq : (fun x => [(pVerdict tm wp tp x).headD false]) = pVerdict tm wp tp := by + funext x + rcases pVerdict_flag tm wp tp x with h | h <;> rw [h] <;> rfl + rw [heq] + exact pVerdict_mem_FP tm wp tp fun n => by rw [hwe]; omega + · rw [← pVerdict_eq_true_iff tm wp tp hdec hwp hq x] + rcases pVerdict_flag tm wp tp x with h | h <;> simp [h] + +end Complexity diff --git a/Complexitylib/Classes/P/Cobham/Internal/PolyLen.lean b/Complexitylib/Classes/P/Cobham/Internal/PolyLen.lean new file mode 100644 index 00000000..9eeb09dc --- /dev/null +++ b/Complexitylib/Classes/P/Cobham/Internal/PolyLen.lean @@ -0,0 +1,101 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.Algebra +public import Mathlib.Algebra.Polynomial.Eval.Degree + +/-! +# Polynomial lengths inside the algebra + +A computation in Cobham's algebra measures sizes by string lengths, so a +polynomial time or space bound has to be available as a *string of that +length*. `smash` multiplies lengths and concatenation adds them, so Horner's +scheme builds, for any polynomial with natural coefficients, a member of the +algebra whose output has exactly the polynomial's value as its length. + +## Main definitions + +- `Cobham.hornerEval` — Horner evaluation of a coefficient list +- `Cobham.lenOfCoeffs` — the string realizing that value as its length +- `Cobham.polyLen` — the same for a `Polynomial ℕ` + +## Main results + +- `Cobham.lenOfCoeffs_mem`, `Cobham.polyLen_mem` — both are in the algebra +- `Cobham.polyLen_length` — `polyLen q s` has length exactly `q.eval |s|` +-/ + +@[expose] public section + +namespace Complexity + +namespace Cobham + +/-- Horner evaluation of a coefficient list, lowest coefficient first. -/ +def hornerEval : List ℕ → ℕ → ℕ + | [], _ => 0 + | a :: as, n => a + n * hornerEval as n + +/-- The string whose length is the Horner value of the coefficient list at +`|s|`: constants contribute blocks of that many bits, and each multiplication +by `|s|` is one `smash`. -/ +def lenOfCoeffs : List ℕ → List Bool → List Bool + | [], _ => [] + | a :: as, s => List.replicate a false ++ Complexity.smash s (lenOfCoeffs as s) + +@[simp] theorem lenOfCoeffs_length (as : List ℕ) (s : List Bool) : + (lenOfCoeffs as s).length = hornerEval as s.length := by + induction as with + | nil => rfl + | cons a as ih => + rw [lenOfCoeffs, hornerEval, List.length_append, List.length_replicate, + smash_length, ih] + +/-- **The Horner string is in the algebra.** -/ +theorem lenOfCoeffs_mem {n : ℕ} (as : List ℕ) + {g : (Fin n → List Bool) → List Bool} (hg : Cobham g) : + Cobham fun v : Fin n → List Bool => lenOfCoeffs as (g v) := by + induction as with + | nil => exact (Cobham.const []).of_eq fun _ => rfl + | cons a as ih => + exact (appendFn (Cobham.const (List.replicate a false)) + (comp₂ Cobham.smash hg ih)).of_eq fun _ => by simp [lenOfCoeffs] + +/-- Horner evaluation of a truncated coefficient sequence is the truncated +power sum. -/ +theorem hornerEval_map_range (f : ℕ → ℕ) (d n : ℕ) : + hornerEval ((List.range d).map f) n = ∑ i ∈ Finset.range d, f i * n ^ i := by + induction d generalizing f with + | zero => rfl + | succ d ih => + rw [List.range_succ_eq_map, List.map_cons, List.map_map, hornerEval, + ih (f ∘ Nat.succ), Finset.sum_range_succ' (fun i => f i * n ^ i) d] + simp only [Function.comp_apply, pow_zero, mul_one] + have hmul : n * ∑ i ∈ Finset.range d, f (i + 1) * n ^ i + = ∑ i ∈ Finset.range d, f (i + 1) * n ^ (i + 1) := by + rw [Finset.mul_sum] + exact Finset.sum_congr rfl fun i _ => by ring + rw [hmul] + omega + +/-- The string realizing a polynomial's value as its length. -/ +noncomputable def polyLen (q : Polynomial ℕ) (s : List Bool) : List Bool := + lenOfCoeffs ((List.range (q.natDegree + 1)).map q.coeff) s + +/-- **The polynomial's value is the string's length.** -/ +@[simp] theorem polyLen_length (q : Polynomial ℕ) (s : List Bool) : + (polyLen q s).length = q.eval s.length := by + rw [polyLen, lenOfCoeffs_length, hornerEval_map_range, Polynomial.eval_eq_sum_range] + +/-- **The polynomial-length string is in the algebra.** -/ +theorem polyLen_mem {n : ℕ} (q : Polynomial ℕ) + {g : (Fin n → List Bool) → List Bool} (hg : Cobham g) : + Cobham fun v : Fin n → List Bool => polyLen q (g v) := + lenOfCoeffs_mem _ hg + +end Cobham + +end Complexity diff --git a/Complexitylib/Classes/P/Cobham/Internal/PolyRuler.lean b/Complexitylib/Classes/P/Cobham/Internal/PolyRuler.lean new file mode 100644 index 00000000..6369be4d --- /dev/null +++ b/Complexitylib/Classes/P/Cobham/Internal/PolyRuler.lean @@ -0,0 +1,73 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P.Cobham.Internal.PolyLen + +/-! +# Rulers of polynomial length + +A *ruler* is a string of zeros whose only content is its length. `polyRuler q` +has length `q.eval |x|` and is polynomial-time in `x`; `wideRuler m R` is `m` +copies of a ruler. Rulers drive the bounded loops of `Cobham.iterate_mem_FP`: +the loop runs once per ruler symbol. + +## Main definitions + +- `polyRuler`, `polyRulerFn_mem_FP` — a ruler of polynomial length, in `FP` +- `wideRuler`, `wideRulerFn_mem_FP` — a constant number of copies +- `blockRuler_eq_polyRuler` — the block ruler of a polynomial window +-/ + +@[expose] public section + +namespace Complexity +open Cobham + +/-- A ruler whose length is a polynomial in the input length. -/ +def polyRuler (q : Polynomial ℕ) (x : List Bool) : List Bool := + List.replicate (q.eval x.length) false + +@[simp] theorem polyRuler_length (q : Polynomial ℕ) (x : List Bool) : + (polyRuler q x).length = q.eval x.length := by + rw [polyRuler, List.length_replicate] + +theorem polyRulerFn_mem_FP (q : Polynomial ℕ) {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => polyRuler q (a z)) ∈ FP := by + have h : Cobham fun v : Fin 1 → List Bool => + List.replicate (Cobham.polyLen q (v 0)).length false := + Cobham.zeroBlockFn (Cobham.polyLen_mem q (Cobham.proj 0)) + refine unFn_mem_FP (g := polyRuler q) ?_ ha + refine h.of_eq fun v => ?_ + rw [polyRuler, Cobham.polyLen_length] + +/-- The block ruler of a polynomial window. -/ +theorem blockRuler_eq_polyRuler (q : Polynomial ℕ) (x : List Bool) : + blockRuler (q.eval x.length) = polyRuler (2 * q + 2) x := by + rw [blockRuler, polyRuler, blockWidth] + congr 1 + simp [Polynomial.eval_add, Polynomial.eval_mul] + omega + +/-- A code's width, as a ruler: `m` copies of the block ruler. -/ +def wideRuler (m : ℕ) (R : List Bool) : List Bool := (List.replicate m R).flatten + +@[simp] theorem wideRuler_length (m : ℕ) (R : List Bool) : + (wideRuler m R).length = m * R.length := by + rw [wideRuler, List.length_flatten] + simp [List.sum_replicate] + +/-- Every constant number of copies of a polynomial-time value is +polynomial-time. -/ +theorem wideRulerFn_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) (m : ℕ) : + (fun z => wideRuler m (a z)) ∈ FP := by + induction m with + | zero => exact mem_FP_of_eq (constFn_mem_FP []) (fun _ => rfl) + | succ m ih => + refine mem_FP_of_eq (Cobham.appendFn_mem_FP ha ih) fun z => ?_ + simp only [wideRuler, List.replicate_succ, List.flatten_cons] + +end Complexity diff --git a/Complexitylib/Classes/P/Cobham/Internal/StringOps.lean b/Complexitylib/Classes/P/Cobham/Internal/StringOps.lean new file mode 100644 index 00000000..e9ab922c --- /dev/null +++ b/Complexitylib/Classes/P/Cobham/Internal/StringOps.lean @@ -0,0 +1,328 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.Algebra + +/-! +# Length tests and bitwise operations inside the algebra + +Two utilities the algebra needs for block-structured data: comparing the +lengths of two strings, and taking the bitwise exclusive-or of two strings of +equal length. + +Both are built from the dispatchers of +`Complexitylib.Classes.P.Cobham.Internal.Blocks`: a length comparison is a +`drop` followed by an emptiness test, and the exclusive-or is one limited +recursion whose step reads the matching bit of the second argument through a +ruler cut to the right width. + +## Main definitions + +- `Cobham.lenLeFlag`, `Cobham.lenEqFlag` — length comparison flags +- `Cobham.xorSuffix` — exclusive-or of a string with the matching suffix of a + second string + +## Main results + +- `Cobham.lenLeFlag_eq_true_iff`, `Cobham.lenEqFlag_eq_true_iff` +- `Cobham.lenLeFlag_mem`, `Cobham.lenEqFlag_mem` +- `Cobham.andBit_eq_true_iff` — conjunction of flags +-/ + +@[expose] public section + +namespace Complexity + +namespace Cobham + +/-- Conjunction of two flags is `[true]` exactly when both are. -/ +theorem andBit_eq_true_iff {x y : List Bool} + (hx : x = [true] ∨ x = [false]) (hy : y = [true] ∨ y = [false]) : + andBit x y = [true] ↔ x = [true] ∧ y = [true] := by + rcases hx with rfl | rfl <;> rcases hy with rfl | rfl <;> simp [andBit] + +/-- The flag `|b| ≤ |a|`: nothing is left of `b` after dropping `|a|` bits. -/ +def lenLeFlag (a b : List Bool) : List Bool := notBit (nonemptyFlag (b.drop a.length)) + +/-- The flag `|a| = |b|`. -/ +def lenEqFlag (a b : List Bool) : List Bool := andBit (lenLeFlag a b) (lenLeFlag b a) + +theorem lenLeFlag_flag (a b : List Bool) : + lenLeFlag a b = [true] ∨ lenLeFlag a b = [false] := by + rw [lenLeFlag, nonemptyFlag] + rcases hb : b.drop a.length with _ | ⟨c, z⟩ + · exact Or.inl rfl + · cases c <;> exact Or.inr rfl + +@[simp] theorem lenLeFlag_eq_true_iff (a b : List Bool) : + lenLeFlag a b = [true] ↔ b.length ≤ a.length := by + rw [lenLeFlag, nonemptyFlag] + rcases hb : b.drop a.length with _ | ⟨c, z⟩ + · have : b.length ≤ a.length := by + have := List.length_drop (i := a.length) (l := b) + rw [hb] at this + simp at this + omega + simp [notBit, this] + · have hlt : a.length < b.length := by + have hlen := List.length_drop (i := a.length) (l := b) + rw [hb] at hlen + simp at hlen + omega + cases c <;> simp [notBit, Nat.not_le.mpr hlt] + +theorem lenEqFlag_flag (a b : List Bool) : + lenEqFlag a b = [true] ∨ lenEqFlag a b = [false] := by + rw [lenEqFlag] + rcases lenLeFlag_flag a b with h | h <;> rcases lenLeFlag_flag b a with h' | h' <;> + rw [h, h'] <;> simp [andBit] + +@[simp] theorem lenEqFlag_eq_true_iff (a b : List Bool) : + lenEqFlag a b = [true] ↔ a.length = b.length := by + rw [lenEqFlag, andBit_eq_true_iff (lenLeFlag_flag a b) (lenLeFlag_flag b a), + lenLeFlag_eq_true_iff, lenLeFlag_eq_true_iff] + omega + +/-- **The length tests are in the algebra.** -/ +theorem lenLeFlag_mem {n : ℕ} {ga gb : (Fin n → List Bool) → List Bool} + (ha : Cobham ga) (hb : Cobham gb) : + Cobham fun v : Fin n → List Bool => lenLeFlag (ga v) (gb v) := + (notFn (nonemptyFn (dropFn ha hb))).of_eq fun _ => rfl + +theorem lenEqFlag_mem {n : ℕ} {ga gb : (Fin n → List Bool) → List Bool} + (ha : Cobham ga) (hb : Cobham gb) : + Cobham fun v : Fin n → List Bool => lenEqFlag (ga v) (gb v) := + (andFn (lenLeFlag_mem ha hb) (lenLeFlag_mem hb ha)).of_eq fun _ => rfl + +/-! ## Exclusive-or -/ + +/-- Exclusive-or of `a` with the suffix of `b` of the same length. The bit of +`b` paired with the head of `a` sits at index `|b| - |x| - 1`, which is the +length of `b.drop (|x| + 1)` — a ruler the algebra can build from the +recursion's own tail. -/ +def xorSuffix : List Bool → List Bool → List Bool + | [], _ => [] + | true :: x, b => + caseBit₀ (notBit (bitAt (b.drop (false :: x).length) b)) + (true :: xorSuffix x b) (false :: xorSuffix x b) + | false :: x, b => + caseBit₀ (bitAt (b.drop (false :: x).length) b) + (true :: xorSuffix x b) (false :: xorSuffix x b) + +@[simp] theorem xorSuffix_nil (b : List Bool) : xorSuffix [] b = [] := rfl + +@[simp] theorem xorSuffix_length (a b : List Bool) : + (xorSuffix a b).length = a.length := by + induction a with + | nil => rfl + | cons β x ih => + cases β <;> + · rw [xorSuffix] + rcases hb : (bitAt (b.drop (false :: x).length) b) with _ | ⟨d, z⟩ + · simp [notBit, ih] + · cases d <;> simp [notBit, ih] + +/-- The bit read at the matching position. -/ +private theorem bitAt_drop_eq (x b : List Bool) (h : x.length < b.length) : + bitAt (b.drop (false :: x).length) b + = [b[b.length - x.length - 1]'(by omega)] := by + have hlen : (b.drop (false :: x).length).length = b.length - x.length - 1 := by + rw [List.length_drop, List.length_cons] + omega + rw [bitAt, hlen] + have hd : b.drop (b.length - x.length - 1) + = b[b.length - x.length - 1]'(by omega) :: b.drop (b.length - x.length) := by + have hcons := List.drop_eq_getElem_cons (l := b) (i := b.length - x.length - 1) + (by omega) + rw [hcons, show b.length - x.length - 1 + 1 = b.length - x.length from by omega] + rw [hd] + cases b[b.length - x.length - 1]'(by omega) <;> rfl + +/-- **The exclusive-or is the pointwise one against the matching suffix.** -/ +theorem xorSuffix_eq_zipWith (a b : List Bool) (h : a.length ≤ b.length) : + xorSuffix a b = List.zipWith xor a (b.drop (b.length - a.length)) := by + induction a with + | nil => simp + | cons β x ih => + have hx : x.length < b.length := by + rw [List.length_cons] at h + omega + have hdrop : b.drop (b.length - (β :: x).length) + = b[b.length - x.length - 1]'(by omega) :: b.drop (b.length - x.length) := by + have h1 : b.length - (β :: x).length = b.length - x.length - 1 := by + rw [List.length_cons] + omega + have hcons := List.drop_eq_getElem_cons (l := b) (i := b.length - x.length - 1) + (by omega) + rw [h1, hcons, show b.length - x.length - 1 + 1 = b.length - x.length from by omega] + rw [hdrop, List.zipWith_cons_cons, ← ih (by omega)] + cases β <;> + · rw [xorSuffix, bitAt_drop_eq x b hx] + cases b[b.length - x.length - 1]'(by omega) <;> simp [notBit] + +/-- Two strings of equal length are combined bit by bit. -/ +theorem xorSuffix_eq_zipWith_of_length (a b : List Bool) (h : a.length = b.length) : + xorSuffix a b = List.zipWith xor a b := by + rw [xorSuffix_eq_zipWith a b h.le, h] + simp + +/-- The step functions of the exclusive-or recursion. -/ +private def xorStep (β : Bool) (w : Fin 3 → List Bool) : List Bool := + caseBit₀ + ((bif β then notBit else id) (bitAt ((w 2).drop (false :: w 0).length) (w 2))) + (true :: w 1) (false :: w 1) + +private theorem xorStep_mem (β : Bool) : Cobham (xorStep β) := by + have hprepend : Cobham fun w : Fin 3 → List Bool => false :: w 0 := + (Cobham.comp (Cobham.bit false) fun _ : Fin 1 => Cobham.proj 0).of_eq fun _ => rfl + have hbit : Cobham fun w : Fin 3 → List Bool => + bitAt ((w 2).drop (false :: w 0).length) (w 2) := + (comp₂ bitAtFn (dropFn hprepend (Cobham.proj 2)) (Cobham.proj 2)).of_eq fun _ => rfl + have hcons1 : Cobham fun w : Fin 3 → List Bool => true :: w 1 := + (Cobham.comp (Cobham.bit true) fun _ : Fin 1 => Cobham.proj 1).of_eq fun _ => rfl + have hcons0 : Cobham fun w : Fin 3 → List Bool => false :: w 1 := + (Cobham.comp (Cobham.bit false) fun _ : Fin 1 => Cobham.proj 1).of_eq fun _ => rfl + cases β + · exact (iteFn hbit hcons1 hcons0).of_eq fun _ => rfl + · exact (iteFn (notFn hbit) hcons1 hcons0).of_eq fun _ => rfl + +private theorem recNotation_xor (a b : List Bool) : + recNotation (fun _ : Fin 1 → List Bool => ([] : List Bool)) (xorStep false) + (xorStep true) a (fun _ => b) = xorSuffix a b := by + induction a with + | nil => rfl + | cons β x ih => + cases β <;> + · rw [recNotation_cons, xorSuffix] + simp only [cond_false, cond_true, xorStep, Fin.cons_zero, Fin.cons_one, id] + rw [ih] + rfl + +private theorem recNotation_xor_length (a b : List Bool) : + (recNotation (fun _ : Fin 1 → List Bool => ([] : List Bool)) (xorStep false) + (xorStep true) a (fun _ => b)).length ≤ a.length := by + rw [recNotation_xor, xorSuffix_length] + +/-- **The exclusive-or is in the algebra.** -/ +theorem xorSuffix_mem {n : ℕ} {ga gb : (Fin n → List Bool) → List Bool} + (ha : Cobham ga) (hb : Cobham gb) : + Cobham fun v : Fin n → List Bool => xorSuffix (ga v) (gb v) := by + have hrec := Cobham.boundedRec (g := fun _ : Fin 1 → List Bool => ([] : List Bool)) + (h₀ := xorStep false) (h₁ := xorStep true) + (j := fun w : Fin 2 → List Bool => w 0) + Cobham.empty (xorStep_mem false) (xorStep_mem true) (Cobham.proj 0) + (by + intro x v + have := recNotation_xor_length x (v 0) + have hv : (fun _ : Fin 1 => v 0) = v := by + funext i + rw [Subsingleton.elim i 0] + rw [hv] at this + simpa using this) + have hg : ∀ i : Fin 2, Cobham (![ga, gb] i) := by + intro i + match i with + | 0 => exact ha + | 1 => exact hb + refine (Cobham.comp hrec hg).of_eq fun v => ?_ + have hb2 : (Fin.tail fun i => ![ga, gb] i v) = fun _ : Fin 1 => gb v := by + funext i + rw [Subsingleton.elim i 0] + rfl + show recNotation _ _ _ (ga v) (Fin.tail fun i => ![ga, gb] i v) = _ + rw [hb2, recNotation_xor] + +/-! ## Equality of strings -/ + +/-- Flag: every bit of `x` is `false`. -/ +def allZeroFlag : List Bool → List Bool + | [] => [true] + | true :: _ => [false] + | false :: x => allZeroFlag x + +@[simp] theorem allZeroFlag_nil : allZeroFlag [] = [true] := rfl + +theorem allZeroFlag_flag (x : List Bool) : allZeroFlag x = [true] ∨ allZeroFlag x = [false] := by + induction x with + | nil => exact Or.inl rfl + | cons b x ih => + cases b + · exact ih + · exact Or.inr rfl + +@[simp] theorem allZeroFlag_eq_true_iff (x : List Bool) : + allZeroFlag x = [true] ↔ ∀ b ∈ x, b = false := by + induction x with + | nil => simp + | cons b x ih => + cases b + · simpa [allZeroFlag] using ih + · simp [allZeroFlag] + +private theorem recNotation_allZero (x : List Bool) (v : Fin 0 → List Bool) : + recNotation (fun _ : Fin 0 → List Bool => ([true] : List Bool)) + (fun w : Fin 2 → List Bool => w 1) + (fun _ : Fin 2 → List Bool => ([false] : List Bool)) x v = allZeroFlag x := by + induction x with + | nil => rfl + | cons b x ih => + cases b + · simpa [allZeroFlag] using ih + · rfl + +/-- **The all-zero test is in the algebra.** -/ +theorem allZeroFlag_mem_one : Cobham fun v : Fin 1 → List Bool => allZeroFlag (v 0) := by + have hrec := Cobham.boundedRec (g := fun _ : Fin 0 → List Bool => ([true] : List Bool)) + (h₀ := fun w : Fin 2 → List Bool => w 1) + (h₁ := fun _ : Fin 2 → List Bool => ([false] : List Bool)) + (j := fun _ : Fin 1 → List Bool => ([false] : List Bool)) + (Cobham.const _) (Cobham.proj 1) (Cobham.const _) (Cobham.const _) + (by + intro x v + rw [recNotation_allZero] + rcases allZeroFlag_flag x with h | h <;> simp [h]) + exact hrec.of_eq fun v => recNotation_allZero (v 0) _ + +theorem allZeroFlag_mem {n : ℕ} {g : (Fin n → List Bool) → List Bool} (hg : Cobham g) : + Cobham fun v : Fin n → List Bool => allZeroFlag (g v) := + (Cobham.comp allZeroFlag_mem_one fun _ : Fin 1 => hg).of_eq fun _ => rfl + +/-- Flag: the two strings are equal. -/ +def eqFlag (a b : List Bool) : List Bool := andBit (lenEqFlag a b) (allZeroFlag (xorSuffix a b)) + +theorem eqFlag_flag (a b : List Bool) : eqFlag a b = [true] ∨ eqFlag a b = [false] := + andBit_flag _ _ + +@[simp] theorem eqFlag_eq_true_iff (a b : List Bool) : eqFlag a b = [true] ↔ a = b := by + rw [eqFlag, andBit_eq_true_iff (lenEqFlag_flag a b) (allZeroFlag_flag _), + lenEqFlag_eq_true_iff, allZeroFlag_eq_true_iff] + constructor + · rintro ⟨hlen, hzero⟩ + rw [xorSuffix_eq_zipWith_of_length a b hlen] at hzero + refine List.ext_getElem hlen fun i h₁ h₂ => ?_ + have hlt : i < (List.zipWith xor a b).length := by + rw [List.length_zipWith] + omega + have hmem := List.getElem_mem hlt + have hval := hzero _ hmem + rw [List.getElem_zipWith] at hval + cases ha : a[i] <;> cases hb : b[i] <;> simp_all + · rintro rfl + refine ⟨rfl, fun c hc => ?_⟩ + rw [xorSuffix_eq_zipWith_of_length a a rfl] at hc + obtain ⟨i, hi, rfl⟩ := List.getElem_of_mem hc + rw [List.getElem_zipWith] + simp + +/-- **String equality is in the algebra.** -/ +theorem eqFlag_mem {n : ℕ} {ga gb : (Fin n → List Bool) → List Bool} + (ha : Cobham ga) (hb : Cobham gb) : + Cobham fun v : Fin n → List Bool => eqFlag (ga v) (gb v) := + (andFn (lenEqFlag_mem ha hb) (allZeroFlag_mem (xorSuffix_mem ha hb))).of_eq fun _ => rfl + +end Cobham + +end Complexity diff --git a/Complexitylib/Classes/P/DecisionFn.lean b/Complexitylib/Classes/P/DecisionFn.lean new file mode 100644 index 00000000..8c0a4632 --- /dev/null +++ b/Complexitylib/Classes/P/DecisionFn.lean @@ -0,0 +1,52 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Preimage +public import Complexitylib.Languages.Contains + +/-! +# From a polynomial-time decision function to membership in `P` + +A language whose verdict is computed by a polynomial-time *function* is in +`P`. This is the bridge that lets a development establish membership in `P` +by exhibiting a function — in particular by building one in Cobham's algebra, +where `Complexitylib.Classes.P.Cobham` supplies `CobhamFP_eq_FP` — instead of +constructing a decider machine by hand. + +The proof reads the verdict off the output through the polynomial-time +language `Language.containsOne`: a verdict string is accepted exactly when it +contains a `1`, and `P` is closed under polynomial-time preimages. + +## Main results + +- `mem_P_of_decisionFn` — a verdict function in `FP` puts its language in `P` +- `mem_P_of_decisionFn_bool` — the same with a `Bool`-valued verdict +-/ + +@[expose] public section + +namespace Complexity + +/-- **A polynomial-time verdict function decides a polynomial-time language.** +If `f ∈ FP` and `x ∈ L` exactly when `f x` contains a `1`-bit, then `L ∈ P`. -/ +theorem mem_P_of_decisionFn {f : List Bool → List Bool} {L : Language} + (hf : f ∈ FP) (hL : ∀ x, x ∈ L ↔ ∃ b ∈ f x, b = true) : L ∈ P := by + have hpre : L = f ⁻¹' Language.containsOne := by + ext x + rw [Set.mem_preimage, Language.mem_containsOne] + exact hL x + rw [hpre] + exact mem_P_preimage hf containsOne_mem_P + +/-- The `Bool`-valued form: a polynomial-time function that emits the verdict +as a one-bit string decides its language. -/ +theorem mem_P_of_decisionFn_bool {g : List Bool → Bool} {L : Language} + (hf : (fun x => [g x]) ∈ FP) (hL : ∀ x, x ∈ L ↔ g x = true) : L ∈ P := by + refine mem_P_of_decisionFn hf (fun x => ?_) + rw [hL x] + simp + +end Complexity diff --git a/Complexitylib/Classes/PCP.lean b/Complexitylib/Classes/PCP.lean new file mode 100644 index 00000000..bb255baa --- /dev/null +++ b/Complexitylib/Classes/PCP.lean @@ -0,0 +1,47 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Defs +public import Complexitylib.Classes.PCP.Internal + +/-! +# The PCP theorem + +`PCP r q` and its verifiers are defined in `Complexitylib.Classes.PCP.Defs`; +this file states and proves the theorem itself. + +Both inclusions are hard work. `NP ⊆ PCP[O(log n), O(1)]` is Dinur's gap +amplification, carried out by the modules under `PCP/Internal`: a formula +becomes a constraint graph, the graph is amplified logarithmically many times +until its unsatisfiability value is a constant, and a verifier reads one edge of +the result. `PCP[O(log n), O(1)] ⊆ NP` guesses the whole proof table. + +## Main results + +- `Complexity.PCP_theorem` — `NP = PCP[O(log n), O(1)]` +-/ + +@[expose] public section + +namespace Complexity + +/-! ## The PCP theorem -/ + +/-- **The PCP theorem**, as stated on +: `NP = PCP[O(log n), O(1)]`. The +big-O classes are unions over all functions `r =O log` and `q =O 1` in the +library's `BigO` (eventual domination up to a constant), with the randomness +bound required to be `Constructible` — see that definition for why the +requirement cannot be dropped. -/ +theorem PCP_theorem : + NP = ⋃ (r : ℕ → ℕ) (_ : r =O Nat.log 2) (_ : Constructible r) + (q : ℕ → ℕ) (_ : q =O fun _ => 1), PCP r q := by + ext L + simp only [Set.mem_iUnion, exists_prop] + exact ⟨exists_pcp_of_mem_NP, + fun ⟨_, hrlog, hrc, _, hq1, hmem⟩ => PCP_subset_NP hrc hrlog hq1 hmem⟩ + +end Complexity diff --git a/Complexitylib/Classes/PCP/Defs.lean b/Complexitylib/Classes/PCP/Defs.lean new file mode 100644 index 00000000..61b02783 --- /dev/null +++ b/Complexitylib/Classes/PCP/Defs.lean @@ -0,0 +1,188 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.EventProb +public import Complexitylib.Classes.NP +public import Complexitylib.Classes.P.Cobham +public import Complexitylib.Classes.P.Composition +public import Complexitylib.Classes.P.Cobham.Internal.ConsBit +public import Complexitylib.Classes.P.Cobham.Internal.FstBlock +public import Complexitylib.Classes.P.Cobham.Internal.Vec +public import Complexitylib.Classes.P.Preimage +public import Complexitylib.Circuits.BitString +public import Complexitylib.Encoding.DataEncode + +/-! +# Probabilistically checkable proofs: definitions + +`PCP r q` is the class of languages with a probabilistically checkable proof +system: a polynomial-time verifier that, on an input of length `n`, flips +`r n` coins, reads at most `q n` bits of a proof string, always accepts a +correct proof of a member, and rejects every purported proof of a non-member +with probability at least `1/2`. This is the class `PCP[r(n), q(n)]` of +Arora–Barak, Definition 11.5, and of +: "the class of problems for +which a probabilistically checkable proof of a solution can be given, such that +the proof can be checked in polynomial time using `r(n)` bits of randomness and +by reading `q(n)` bits of the proof, correct proofs are always accepted, and +incorrect proofs are rejected with probability at least `1/2`." + +As in `Complexitylib.Classes.Interactive`, the verifier is machine-free: its +two computations are a query-selection function in `FP` and a verdict language +in `P`, both applied to encoded tuples. The verifier is *non-adaptive* — the +positions it reads are a function of the input and the coins alone, not of +earlier answers — which is the standard choice (Arora–Barak, Definition 11.5) +and costs only a `2^q` blow-up in query count against the adaptive variant. + +## Main definitions + +- `PCPVerifier` — query positions as an `FP`-computable function of the encoded + input and coins, and a verdict in `P` on the input, the coins, and the bits + read +- `PCPVerifier.Accepts`, `PCPVerifier.acceptEvent` +- `PCP` — the class `PCP[r(n), q(n)]` + +## Main results + +- `PCP_mono` — more coins or more queries only enlarge the class +- `P_subset_PCP` — the definition contains `P` for every `r` and `q`: the + verifier reads nothing and ignores its coins +- `Constructible` — a resource bound that can be written out in unary + +## Conventions + +The verifier uses *exactly* `r n` coins, never fewer: a verifier wanting fewer +can ignore the surplus, so this loses nothing, and it keeps the coin count a +parameter of the class rather than a field the verifier could smuggle +non-uniform information into. The query count is an upper bound `≤ q n` since +the position list is produced by the `FP` function and carries no hidden +information. + +The proof is a finite string; a position beyond its end reads as `false`. This +is no restriction, as the verifier's positions are polynomially many bits long, +so a proof of length `q n · 2 ^ r n` suffices, matching the usual convention. + +Completeness `1` and soundness `1/2` are hard-wired, following the +`PCP[r, q]` convention rather than the `2/3`–`1/3` of +`Complexitylib.Classes.Randomized`. + +## TODO + +- Prove the PCP theorem. +-/ + +@[expose] public section + +namespace Complexity + +/-! ## Verifiers -/ + +/-- A (non-adaptive) PCP verifier: from the encoded input and coins it computes +a list of proof positions in polynomial time, and from the input, the coins, and +the bits found there it decides in polynomial time. -/ +structure PCPVerifier where + /-- The proof positions queried on input `x` with coins `r`. -/ + positions : List Bool → List Bool → List ℕ + /-- That computation is polynomial-time, as a function of `pair x r` producing + the `DataEncode` bitstring of the position list. -/ + positions_mem : ∃ f ∈ FP, ∀ x r : List Bool, + f (pair x r) = DataEncode.bitstringEncode (positions x r) + /-- The verdict, on `pair (pair x r) a` where `a` lists the bits read. -/ + verdict : Language + /-- That verdict is polynomial-time decidable. -/ + verdict_mem : verdict ∈ P + +namespace PCPVerifier + +/-- The bits of the proof `π` at the listed positions; a position past the end +of the proof reads as `false`. -/ +def answers (π : List Bool) (ps : List ℕ) : List Bool := + ps.map fun i => π.getD i false + +/-- The verifier accepts input `x` and proof `π` with coins `r`. -/ +def Accepts (V : PCPVerifier) (x π r : List Bool) : Prop := + pair (pair x r) (answers π (V.positions x r)) ∈ V.verdict + +open Classical in +/-- The coin strings of length `t` on which `V` accepts `x` with proof `π`. -/ +noncomputable def acceptEvent (V : PCPVerifier) (t : ℕ) (x π : List Bool) : + Finset (Fin t → Bool) := + Finset.univ.filter fun r => V.Accepts x π (BitString.toList r) + +/-- The verifier reads at most `q n` bits of the proof on inputs of length `n`, +whatever its coins. -/ +def QueryBounded (V : PCPVerifier) (q : ℕ → ℕ) : Prop := + ∀ x r : List Bool, (V.positions x r).length ≤ q x.length + +end PCPVerifier + +/-! ## The class -/ + +/-- **`PCP[r(n), q(n)]`**: languages with a polynomial-time verifier using `r n` +random bits and reading at most `q n` bits of the proof, such that a member has +a proof the verifier always accepts, while every proof of a non-member is +rejected with probability at least `1/2`. -/ +def PCP (r q : ℕ → ℕ) : Set Language := + {L | ∃ V : PCPVerifier, V.QueryBounded q ∧ + (∀ x ∈ L, ∃ π : List Bool, eventProb (V.acceptEvent (r x.length) x π) = 1) ∧ + (∀ x ∉ L, ∀ π : List Bool, eventProb (V.acceptEvent (r x.length) x π) ≤ 1 / 2)} + +/-! ## Elementary properties -/ + +/-- The verifier that reads no bits of the proof, ignores its coins, and +decides `L` on the input it recovers from the encoded view. -/ +private noncomputable def inputVerifier (L : Language) (hL : L ∈ P) : PCPVerifier where + positions _ _ := [] + positions_mem := + ⟨fun _ => DataEncode.bitstringEncode ([] : List ℕ), + constFn_mem_FP _, fun _ _ => rfl⟩ + verdict := (fun z => pairFst (pairFst z)) ⁻¹' L + verdict_mem := by + refine mem_P_preimage ?_ hL + exact mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + +/-- **`P ⊆ PCP[r, q]`** for every `r` and `q`: the verifier never looks at the +proof or its coins. -/ +theorem P_subset_PCP (r q : ℕ → ℕ) : P ⊆ PCP r q := by + intro L hL + refine ⟨inputVerifier L hL, fun _ _ => by simp [inputVerifier], ?_, ?_⟩ + · intro x hx + refine ⟨[], ?_⟩ + have hev : (inputVerifier L hL).acceptEvent (r x.length) x [] = Finset.univ := by + ext ρ + simp [PCPVerifier.acceptEvent, PCPVerifier.Accepts, inputVerifier, hx] + rw [hev, eventProb_univ] + · intro x hx π + have hev : (inputVerifier L hL).acceptEvent (r x.length) x π = ∅ := by + ext ρ + simp [PCPVerifier.acceptEvent, PCPVerifier.Accepts, inputVerifier, hx] + rw [hev, eventProb_empty] + norm_num + +/-- More queries only enlarge the class. -/ +theorem PCP_mono_queries {r q q' : ℕ → ℕ} (hq : ∀ n, q n ≤ q' n) : + PCP r q ⊆ PCP r q' := by + rintro L ⟨V, hV, hc, hs⟩ + exact ⟨V, fun x ρ => (hV x ρ).trans (hq _), hc, hs⟩ + +/-! ## Constructible bounds -/ + +/-- A resource bound is **constructible** when it can be written out in unary in +polynomial time. + +Some such requirement is not optional. `PCP r q` constrains the verifier but says +nothing about `r`, so without it the union below is not a complexity class at +all: `Complexitylib.Classes.PCP.Internal.BoundNotConstructible` proves that for +*every* set `A ⊆ ℕ`, computable or not, the language of inputs whose length lies +in `A` satisfies the `PCP` conditions with `r` the indicator of `A` — a bound +that is `O(1)`, hence `O(log n)`. That puts continuum-many languages in the +union while `NP` is countable, so the unrestricted equation is false. Textbook +statements of the theorem carry the same requirement tacitly, by taking the +bounds to be constructible functions. -/ +def Constructible (r : ℕ → ℕ) : Prop := + (fun x : List Bool => List.replicate (r x.length) true) ∈ FP + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal.lean b/Complexitylib/Classes/PCP/Internal.lean new file mode 100644 index 00000000..e84a3803 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal.lean @@ -0,0 +1,128 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.AlgCSPModel +public import Complexitylib.Classes.PCP.Internal.AlgCompose +public import Complexitylib.Classes.PCP.Internal.AlgEdge +public import Complexitylib.Classes.PCP.Internal.AlgFamily +public import Complexitylib.Classes.PCP.Internal.AlgFormula +public import Complexitylib.Classes.PCP.Internal.AlgGap +public import Complexitylib.Classes.PCP.Internal.AlgGapAll +public import Complexitylib.Classes.PCP.Internal.AlgGapCSP +public import Complexitylib.Classes.PCP.Internal.AlgGraph +public import Complexitylib.Classes.PCP.Internal.AlgInit +public import Complexitylib.Classes.PCP.Internal.AlgIter +public import Complexitylib.Classes.PCP.Internal.AlgKey +public import Complexitylib.Classes.PCP.Internal.AlgKeyFn +public import Complexitylib.Classes.PCP.Internal.AlgKilled +public import Complexitylib.Classes.PCP.Internal.AlgLevel +public import Complexitylib.Classes.PCP.Internal.AlgLog +public import Complexitylib.Classes.PCP.Internal.AlgPCP +public import Complexitylib.Classes.PCP.Internal.AlgPosNum +public import Complexitylib.Classes.PCP.Internal.AlgPreRel +public import Complexitylib.Classes.PCP.Internal.AlgPreRot +public import Complexitylib.Classes.PCP.Internal.AlgPreprocess +public import Complexitylib.Classes.PCP.Internal.AlgRound +public import Complexitylib.Classes.PCP.Internal.AlgStep +public import Complexitylib.Classes.PCP.Internal.AlgUniform +public import Complexitylib.Classes.PCP.Internal.AlgWalk +public import Complexitylib.Classes.PCP.Internal.AlphabetLift +public import Complexitylib.Classes.PCP.Internal.Amplification +public import Complexitylib.Classes.PCP.Internal.Arithmetize +public import Complexitylib.Classes.PCP.Internal.BaseAlg +public import Complexitylib.Classes.PCP.Internal.BinToUnary +public import Complexitylib.Classes.PCP.Internal.BitwiseFP +public import Complexitylib.Classes.PCP.Internal.BoundedQuant +public import Complexitylib.Classes.PCP.Internal.CNFCount +public import Complexitylib.Classes.PCP.Internal.CNFMaxVar +public import Complexitylib.Classes.PCP.Internal.CNFSegment +public import Complexitylib.Classes.PCP.Internal.CNFTokens +public import Complexitylib.Classes.PCP.Internal.CSPVerifier +public import Complexitylib.Classes.PCP.Internal.Cheeger +public import Complexitylib.Classes.PCP.Internal.CloudCount +public import Complexitylib.Classes.PCP.Internal.CloudDisagreement +public import Complexitylib.Classes.PCP.Internal.CoinEnum +public import Complexitylib.Classes.PCP.Internal.Compose +public import Complexitylib.Classes.PCP.Internal.ConsistencyLang +public import Complexitylib.Classes.PCP.Internal.ConstraintGraph +public import Complexitylib.Classes.PCP.Internal.CubeBlocks +public import Complexitylib.Classes.PCP.Internal.DartCorrelation +public import Complexitylib.Classes.PCP.Internal.DataScan +public import Complexitylib.Classes.PCP.Internal.DataScanSpec +public import Complexitylib.Classes.PCP.Internal.DegreeReduction +public import Complexitylib.Classes.PCP.Internal.DegreeReductionSound +public import Complexitylib.Classes.PCP.Internal.Dinur +public import Complexitylib.Classes.PCP.Internal.EdgeExpansion +public import Complexitylib.Classes.PCP.Internal.Expander +public import Complexitylib.Classes.PCP.Internal.ExpanderExists +public import Complexitylib.Classes.PCP.Internal.ExpanderMerge +public import Complexitylib.Classes.PCP.Internal.ExpanderPad +public import Complexitylib.Classes.PCP.Internal.ExpanderRandom +public import Complexitylib.Classes.PCP.Internal.ExpanderizeCSP +public import Complexitylib.Classes.PCP.Internal.FamilyFin +public import Complexitylib.Classes.PCP.Internal.FiniteKey +public import Complexitylib.Classes.PCP.Internal.FinsetPlurality +public import Complexitylib.Classes.PCP.Internal.GapReduction +public import Complexitylib.Classes.PCP.Internal.GuessVerifyGeneric +public import Complexitylib.Classes.PCP.Internal.Hadamard +public import Complexitylib.Classes.PCP.Internal.HadamardTester +public import Complexitylib.Classes.PCP.Internal.KilledCSP +public import Complexitylib.Classes.PCP.Internal.KilledFirstMoment +public import Complexitylib.Classes.PCP.Internal.KilledPlurality +public import Complexitylib.Classes.PCP.Internal.KilledWalk +public import Complexitylib.Classes.PCP.Internal.ListEncode +public import Complexitylib.Classes.PCP.Internal.LocalTest +public import Complexitylib.Classes.PCP.Internal.Materialize +public import Complexitylib.Classes.PCP.Internal.MaxLoop +public import Complexitylib.Classes.PCP.Internal.MergeGen +public import Complexitylib.Classes.PCP.Internal.Mixing +public import Complexitylib.Classes.PCP.Internal.NatEncode +public import Complexitylib.Classes.PCP.Internal.NumEnc +public import Complexitylib.Classes.PCP.Internal.NumEncPi +public import Complexitylib.Classes.PCP.Internal.PCPtoSAT +public import Complexitylib.Classes.PCP.Internal.PadGraph +public import Complexitylib.Classes.PCP.Internal.PermArith +public import Complexitylib.Classes.PCP.Internal.PermCount +public import Complexitylib.Classes.PCP.Internal.PermGraph +public import Complexitylib.Classes.PCP.Internal.PosScan +public import Complexitylib.Classes.PCP.Internal.PositionsFP +public import Complexitylib.Classes.PCP.Internal.Power +public import Complexitylib.Classes.PCP.Internal.PoweringBound +public import Complexitylib.Classes.PCP.Internal.Preprocess +public import Complexitylib.Classes.PCP.Internal.RankCount +public import Complexitylib.Classes.PCP.Internal.RegCSP +public import Complexitylib.Classes.PCP.Internal.RegularGraph +public import Complexitylib.Classes.PCP.Internal.SecondMoment +public import Complexitylib.Classes.PCP.Internal.SelfLoops +public import Complexitylib.Classes.PCP.Internal.SquareVerifier +public import Complexitylib.Classes.PCP.Internal.StripTrailing +public import Complexitylib.Classes.PCP.Internal.SubsetNP +public import Complexitylib.Classes.PCP.Internal.SubsetNPFinal +public import Complexitylib.Classes.PCP.Internal.SymbolCodec +public import Complexitylib.Classes.PCP.Internal.TesterChecks +public import Complexitylib.Classes.PCP.Internal.TesterCore +public import Complexitylib.Classes.PCP.Internal.ThreeSATReduction +public import Complexitylib.Classes.PCP.Internal.TowerFin +public import Complexitylib.Classes.PCP.Internal.TowerTable +public import Complexitylib.Classes.PCP.Internal.UnaryDivMod +public import Complexitylib.Classes.PCP.Internal.UnaryExp +public import Complexitylib.Classes.PCP.Internal.UnaryList +public import Complexitylib.Classes.PCP.Internal.Union +public import Complexitylib.Classes.PCP.Internal.VerifierLang +public import Complexitylib.Classes.PCP.Internal.Walk +public import Complexitylib.Classes.PCP.Internal.WalkDart +public import Complexitylib.Classes.PCP.Internal.WalkPath +public import Complexitylib.Classes.PCP.Internal.WalkSplit +public import Complexitylib.Classes.PCP.Internal.ZigZag +public import Complexitylib.Classes.PCP.Internal.ZigZagBaseExists +public import Complexitylib.Classes.PCP.Internal.ZigZagTower + +/-! +# Aggregation: the internals of the PCP development + +Every module of Dinur's proof, so that the whole tree is reachable from a single +build target. Aggregation files carry no definitions. +-/ diff --git a/Complexitylib/Classes/PCP/Internal/AlgCSPModel.lean b/Complexitylib/Classes/PCP/Internal/AlgCSPModel.lean new file mode 100644 index 00000000..0cdc9bd5 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgCSPModel.lean @@ -0,0 +1,319 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.CSPVerifier +public import Complexitylib.Classes.PCP.Internal.ConstraintGraph + +/-! +# Proofs as assignments + +The proof a PCP verifier reads is an assignment written out: one fixed-width +block per vertex. This module sets up that correspondence — how to cut a proof +into blocks, how to write an assignment as a proof, and that the two are +inverse. + +## Main definitions + +- `Complexity.blockOf` — the block a proof carries at a vertex +- `Complexity.proofOf` — the proof an assignment writes + +## Main results + +- `Complexity.blockOf_proofOf` — reading back what was written +- `Complexity.answers_posVal` — the verifier reads exactly the two endpoint + blocks +- `Complexity.AlgCSP.Models` — an algorithmic graph agrees with a real one +- `Complexity.AlgCSP.Models.sat_of_satisfiable` — completeness transfers +-/ + +@[expose] public section + +namespace Complexity + +/-- The block a proof carries at a vertex, when symbols occupy `w` bits. -/ +def blockOf (w : ℕ) (π : List Bool) (v : ℕ) : List Bool := (π.drop (v * w)).take w + +/-- The proof an assignment writes: the blocks of the first `n` vertices, one +after another. -/ +def proofOf (n : ℕ) (f : ℕ → List Bool) : List Bool := (List.range n).flatMap f + +theorem length_proofOf {w : ℕ} {f : ℕ → List Bool} (hf : ∀ i, (f i).length = w) : + ∀ n, (proofOf n f).length = n * w := by + intro n + induction n with + | zero => simp [proofOf] + | succ n ih => + rw [proofOf, List.range_succ, List.flatMap_append, List.length_append] + rw [proofOf] at ih + rw [ih] + simp [hf n] + ring + +/-- **Reading back what was written.** -/ +theorem blockOf_proofOf {w : ℕ} {f : ℕ → List Bool} (hf : ∀ i, (f i).length = w) : + ∀ (n v : ℕ), v < n → blockOf w (proofOf n f) v = f v := by + intro n + induction n with + | zero => intro v hv; omega + | succ n ih => + intro v hv + have hsplit : proofOf (n + 1) f = proofOf n f ++ f n := by + rw [proofOf, proofOf, List.range_succ, List.flatMap_append] + simp + have hlen : (proofOf n f).length = n * w := length_proofOf hf n + rcases Nat.lt_or_ge v n with hlt | hge + · have hvw : v * w + w ≤ n * w := by + have : (v + 1) * w ≤ n * w := Nat.mul_le_mul_right _ (by omega) + rw [Nat.add_mul, Nat.one_mul] at this + omega + rw [blockOf, hsplit, List.drop_append_of_le_length (by omega), + List.take_append_of_le_length (by rw [List.length_drop, hlen]; omega)] + exact ih v hlt + · have hvn : v = n := by omega + subst hvn + rw [blockOf, hsplit, show v * w = (proofOf v f).length from hlen.symm, + List.drop_left, List.take_of_length_le (by rw [hf])] + +theorem length_blockOf {w : ℕ} {π : List Bool} {v : ℕ} (h : (v + 1) * w ≤ π.length) : + (blockOf w π v).length = w := by + rw [blockOf, List.length_take, List.length_drop] + have : v * w + w ≤ π.length := by + rw [Nat.add_mul, Nat.one_mul] at h + omega + omega + +theorem getElem_blockOf {w : ℕ} {π : List Bool} {v j : ℕ} (h : (v + 1) * w ≤ π.length) + (hj : j < w) : + (blockOf w π v)[j]'(by rw [length_blockOf h]; exact hj) = π.getD (v * w + j) false := by + have hlt : v * w + j < π.length := by + rw [Nat.add_mul, Nat.one_mul] at h + omega + simp only [blockOf] + rw [List.getElem_take, List.getElem_drop, List.getD_eq_getElem?_getD, + List.getElem?_eq_getElem hlt] + rfl + +/-- **The verifier reads exactly the two endpoint blocks.** -/ +theorem answers_posVal (A : AlgCSP) (x π : List Bool) (e : ℕ) + (h0 : (A.vert false x e + 1) * A.width ≤ π.length) + (h1 : (A.vert true x e + 1) * A.width ≤ π.length) : + PCPVerifier.answers π ((List.range (2 * A.width)).map (A.posVal x e)) + = blockOf A.width π (A.vert false x e) ++ blockOf A.width π (A.vert true x e) := by + have hb0 : (blockOf A.width π (A.vert false x e)).length = A.width := length_blockOf h0 + have hb1 : (blockOf A.width π (A.vert true x e)).length = A.width := length_blockOf h1 + refine List.ext_getElem ?_ fun j hj1 hj2 => ?_ + · rw [PCPVerifier.answers, List.length_map, List.length_map, List.length_range, + List.length_append, hb0, hb1] + ring + · have hj : j < 2 * A.width := by + rw [PCPVerifier.answers, List.length_map, List.length_map, List.length_range] at hj1 + exact hj1 + have hans : PCPVerifier.answers π ((List.range (2 * A.width)).map (A.posVal x e)) + = ((List.range (2 * A.width)).map (A.posVal x e)).map (fun i => π.getD i false) := + rfl + simp only [hans] + rw [List.getElem_map, List.getElem_map, List.getElem_range] + by_cases hlow : j < A.width + · rw [List.getElem_append_left (by rw [hb0]; exact hlow), getElem_blockOf h0 hlow, + AlgCSP.posVal, if_pos hlow] + simp [hlow] + · have hge : (blockOf A.width π (A.vert false x e)).length ≤ j := by + rw [hb0]; omega + rw [List.getElem_append_right hge] + simp only [hb0] + rw [getElem_blockOf h1 (by omega), AlgCSP.posVal, if_neg hlow] + simp [hlow] + +/-! ### Padding a proof -/ + +theorem answers_append_false (π : List Bool) (k : ℕ) (ps : List ℕ) : + PCPVerifier.answers π ps + = PCPVerifier.answers (π ++ List.replicate k false) ps := by + have hget : ∀ i, π.getD i false = (π ++ List.replicate k false).getD i false := by + intro i + by_cases hi : i < π.length + · rw [List.getD_eq_getElem?_getD, List.getD_eq_getElem?_getD, + List.getElem?_eq_getElem hi, + List.getElem?_eq_getElem (by rw [List.length_append]; omega), + List.getElem_append_left hi] + · rw [List.getD_eq_getElem?_getD, List.getElem?_eq_none (by omega)] + by_cases hi2 : i < (π ++ List.replicate k false).length + · rw [List.getD_eq_getElem?_getD, List.getElem?_eq_getElem hi2, + List.getElem_append_right (by omega)] + simp + · rw [List.getD_eq_getElem?_getD, List.getElem?_eq_none (by omega)] + show ps.map (fun i => π.getD i false) = ps.map _ + exact List.map_congr_left fun i _ => hget i + +/-! ### Agreement with a real constraint graph -/ + +namespace AlgCSP + +/-- An algorithmic graph agrees with a real one: same edges, same endpoints, +and the constraint means the same thing once symbols are decoded. -/ +structure Models (A : AlgCSP) {α : Type} (G : List Bool → ConstraintGraph α) + (enc : α → List Bool) (dec : List Bool → α) : Prop where + /-- The edge counts agree. -/ + numEdges_eq : ∀ x, A.numEdges x = (G x).numEdges + /-- The first endpoint agrees. -/ + tail_eq : ∀ (x : List Bool) (e : ℕ) (he : e < (G x).numEdges), + A.vert false x e = ((G x).tail ⟨e, he⟩).val + /-- The second endpoint agrees. -/ + head_eq : ∀ (x : List Bool) (e : ℕ) (he : e < (G x).numEdges), + A.vert true x e = ((G x).head ⟨e, he⟩).val + /-- Symbols occupy exactly the block width. -/ + length_enc : ∀ s : α, (enc s).length = A.width + /-- Decoding inverts encoding. -/ + dec_enc : ∀ s : α, dec (enc s) = s + /-- The constraint agrees on blocks. -/ + ok_iff : ∀ (x : List Bool) (e : ℕ) (he : e < (G x).numEdges) (u v : List Bool), + u.length = A.width → v.length = A.width → + (pair (pair x (List.replicate e true)) (u ++ v) ∈ A.ok + ↔ (G x).rel ⟨e, he⟩ (dec u) (dec v) = true) + +variable {A : AlgCSP} {α : Type} [Inhabited α] {G : List Bool → ConstraintGraph α} + {enc : α → List Bool} {dec : List Bool → α} + +/-- The proof an assignment writes. -/ +def assignProof (enc : α → List Bool) [Inhabited α] + (n : ℕ) (a : Fin n → α) : List Bool := + proofOf n (fun v => if h : v < n then enc (a ⟨v, h⟩) else enc default) + +theorem length_assignProof (hM : A.Models G enc dec) (n : ℕ) (a : Fin n → α) : + (assignProof enc n a).length = n * A.width := by + refine length_proofOf (w := A.width) (fun i => ?_) n + by_cases h : i < n <;> simp [h, hM.length_enc] + +theorem blockOf_assignProof (hM : A.Models G enc dec) {n : ℕ} (a : Fin n → α) + {v : ℕ} (hv : v < n) : + blockOf A.width (assignProof enc n a) v = enc (a ⟨v, hv⟩) := by + have := blockOf_proofOf (w := A.width) + (f := fun v => if h : v < n then enc (a ⟨v, h⟩) else enc default) + (fun i => by by_cases h : i < n <;> simp [h, hM.length_enc]) n v hv + rw [assignProof, this] + simp only + rw [dif_pos hv] + +/-- **Completeness transfers.** A satisfying assignment writes a proof the +verifier accepts on every edge. -/ +theorem Models.sat_of_satisfiable (hM : A.Models G enc dec) (x : List Bool) + (h : (G x).Satisfiable) : + ∃ π : List Bool, ∀ e < A.numEdges x, A.Sat x π e := by + obtain ⟨a, ha⟩ := h + refine ⟨assignProof enc (G x).numVerts a, ?_⟩ + intro e he + rw [hM.numEdges_eq] at he + set π := assignProof enc (G x).numVerts a with hπ + have hlen : π.length = (G x).numVerts * A.width := length_assignProof hM _ a + have h0 : A.vert false x e = ((G x).tail ⟨e, he⟩).val := hM.tail_eq x e he + have h1 : A.vert true x e = ((G x).head ⟨e, he⟩).val := hM.head_eq x e he + have hb0 : (A.vert false x e + 1) * A.width ≤ π.length := by + rw [hlen, h0] + exact Nat.mul_le_mul_right _ ((G x).tail ⟨e, he⟩).isLt + have hb1 : (A.vert true x e + 1) * A.width ≤ π.length := by + rw [hlen, h1] + exact Nat.mul_le_mul_right _ ((G x).head ⟨e, he⟩).isLt + have hv0 : blockOf A.width π (A.vert false x e) = enc (a ((G x).tail ⟨e, he⟩)) := by + rw [h0, blockOf_assignProof hM a ((G x).tail ⟨e, he⟩).isLt] + have hv1 : blockOf A.width π (A.vert true x e) = enc (a ((G x).head ⟨e, he⟩)) := by + rw [h1, blockOf_assignProof hM a ((G x).head ⟨e, he⟩).isLt] + show pair (pair x (List.replicate e true)) + (PCPVerifier.answers π ((List.range (2 * A.width)).map (A.posVal x e))) ∈ A.ok + rw [answers_posVal A x π e hb0 hb1, hv0, hv1, + hM.ok_iff x e he _ _ (hM.length_enc _) (hM.length_enc _), hM.dec_enc, hM.dec_enc] + exact ha ⟨e, he⟩ + +theorem card_filter_range_eq {n : ℕ} (P : ℕ → Prop) [DecidablePred P] : + ((Finset.range n).filter P).card + = (Finset.univ.filter (fun i : Fin n => P i.val)).card := by + refine Finset.card_bij (fun i hi => (⟨i, Finset.mem_range.mp (Finset.mem_filter.mp hi).1⟩ + : Fin n)) ?_ ?_ ?_ + · intro i hi + simp only [Finset.mem_filter, Finset.mem_univ, true_and] + exact (Finset.mem_filter.mp hi).2 + · intro i hi j hj hij + exact congrArg Fin.val hij + · intro b hb + simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hb + exact ⟨b.val, Finset.mem_filter.mpr ⟨Finset.mem_range.mpr b.isLt, hb⟩, rfl⟩ + +omit [Inhabited α] in +open Classical in +/-- **Soundness transfers.** No proof satisfies more than a `1 - gap` fraction +of the edges. -/ +theorem Models.card_sat_le [Fintype α] [Nonempty α] (hM : A.Models G enc dec) + (x : List Bool) {gap : ℚ} (hgap : gap ≤ (G x).unsatVal) (π : List Bool) : + (((Finset.range (A.numEdges x)).filter (A.Sat x π)).card : ℚ) + ≤ (1 - gap) * A.numEdges x := by + classical + set π' := π ++ List.replicate ((G x).numVerts * A.width) false with hπ' + have hlen : (G x).numVerts * A.width ≤ π'.length := by + rw [hπ', List.length_append, List.length_replicate] + omega + set a : (G x).Assignment := fun v => dec (blockOf A.width π' v.val) with ha + have hstep : ∀ (e : Fin (G x).numEdges), + A.Sat x π e.val ↔ (G x).satisfies a e = true := by + intro e + have h0 : A.vert false x e.val = ((G x).tail e).val := hM.tail_eq x e.val e.isLt + have h1 : A.vert true x e.val = ((G x).head e).val := hM.head_eq x e.val e.isLt + have hb0 : (A.vert false x e.val + 1) * A.width ≤ π'.length := by + refine le_trans (Nat.mul_le_mul_right _ ?_) hlen + rw [h0] + exact ((G x).tail e).isLt + have hb1 : (A.vert true x e.val + 1) * A.width ≤ π'.length := by + refine le_trans (Nat.mul_le_mul_right _ ?_) hlen + rw [h1] + exact ((G x).head e).isLt + show pair (pair x (List.replicate e.val true)) + (PCPVerifier.answers π ((List.range (2 * A.width)).map (A.posVal x e.val))) ∈ A.ok ↔ _ + rw [answers_append_false π ((G x).numVerts * A.width), ← hπ', + answers_posVal A x π' e.val hb0 hb1, + hM.ok_iff x e.val e.isLt _ _ (length_blockOf hb0) (length_blockOf hb1)] + rw [ConstraintGraph.satisfies, ha, h0, h1] + have hcard : ((Finset.range (A.numEdges x)).filter (A.Sat x π)).card + = (Finset.univ.filter + (fun e : Fin (G x).numEdges => (G x).satisfies a e = true)).card := by + rw [hM.numEdges_eq, card_filter_range_eq] + congr 1 + ext e + simp only [Finset.mem_filter, Finset.mem_univ, true_and] + exact hstep e + have hunsat : (G x).unsatEdges a + = Finset.univ.filter (fun e : Fin (G x).numEdges => ¬ ((G x).satisfies a e = true)) := by + ext e + simp [ConstraintGraph.unsatEdges, ConstraintGraph.Satisfies] + have hsplit : (Finset.univ.filter + (fun e : Fin (G x).numEdges => (G x).satisfies a e = true)).card + + ((G x).unsatEdges a).card = (G x).numEdges := by + rw [hunsat] + have h := Finset.card_filter_add_card_filter_not + (s := (Finset.univ : Finset (Fin (G x).numEdges))) + (p := fun e => (G x).satisfies a e = true) + rw [Finset.card_univ, Fintype.card_fin] at h + exact h + rw [hcard, hM.numEdges_eq] + rcases Nat.eq_zero_or_pos (G x).numEdges with h0 | hpos + · have hIE : IsEmpty (Fin (G x).numEdges) := ⟨fun e => absurd e.isLt (by omega)⟩ + have hempty : (Finset.univ.filter + (fun e : Fin (G x).numEdges => (G x).satisfies a e = true)).card = 0 := + Finset.card_eq_zero.mpr (Finset.eq_empty_of_isEmpty _) + rw [hempty, h0] + simp + · have hNQ : (0 : ℚ) < ((G x).numEdges : ℚ) := by exact_mod_cast hpos + have hfrac : gap ≤ ((((G x).unsatEdges a).card : ℚ)) / ((G x).numEdges : ℚ) := + le_trans hgap ((G x).unsatVal_le a) + have hge : gap * ((G x).numEdges : ℚ) ≤ (((G x).unsatEdges a).card : ℚ) := by + rw [le_div_iff₀ hNQ] at hfrac + exact hfrac + have hs : ((Finset.univ.filter + (fun e : Fin (G x).numEdges => (G x).satisfies a e = true)).card : ℚ) + + ((((G x).unsatEdges a).card : ℚ)) = ((G x).numEdges : ℚ) := by + exact_mod_cast congrArg (Nat.cast : ℕ → ℚ) hsplit + linarith + +end AlgCSP + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgCompose.lean b/Complexitylib/Classes/PCP/Internal/AlgCompose.lean new file mode 100644 index 00000000..cc73383b --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgCompose.lean @@ -0,0 +1,78 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.LocalTest +public import Complexitylib.Classes.PCP.Internal.NumEncPi +public import Complexitylib.Classes.PCP.Internal.Compose + +/-! +# The composed graph, in numbers + +The binary graph a family of tests produces has one vertex per position and per +(test, random string), and one edge per (test, random string, read). Both are +numbered by `NumEnc`, so an edge index splits by division and remainder into the +test, the string and the read, and the two endpoints are read off from there. + +## Main results + +- `Complexity.MultiTest.enc_edgeOf` — the edge a number names carries that number +- `Complexity.MultiTest.enc_edge` — how an edge's number splits +- `Complexity.MultiTest.val_tail_toGraph`, `val_head_toGraph` — the endpoints +-/ + +@[expose] public section + +namespace Complexity + +namespace MultiTest + +open NumEnc BooleanAnalysis + +variable {Pos E Q : Type} (M : MultiTest Pos E Q) [Fintype Pos] [Fintype E] [Fintype Q] + [NumEnc Pos] [NumEnc E] [NumEnc Q] + +omit [Fintype Pos] [NumEnc Pos] in +theorem enc_edgeOf (k : Fin (Fintype.card M.Edge)) : enc (M.edgeOf k) = k.val := by + show enc ((NumEnc.equivFinCard M.Edge).symm k) = _ + have h : (NumEnc.equivFinCard M.Edge ((NumEnc.equivFinCard M.Edge).symm k)).val = k.val := + congrArg Fin.val (Equiv.apply_symm_apply _ _) + exact h + +omit [Fintype Pos] [Fintype E] [Fintype Q] [NumEnc Pos] in +/-- **How an edge's number splits**: the test, then the random string, then the +read. -/ +theorem enc_edge (x : M.Edge) : + enc x = enc x.1 * (card (Cube M.R) * card Q) + (enc x.2.1 * card Q + enc x.2.2) := rfl + +/-- **The first endpoint**: the test vertex, numbered after all the positions. -/ +theorem val_tail_toGraph (k : Fin (Fintype.card M.Edge)) : + (M.toGraph.tail k).val + = card Pos + (enc (M.edgeOf k).1 * card (Cube M.R) + enc (M.edgeOf k).2.1) := rfl + +/-- **The second endpoint**: the position the read asks for. -/ +theorem val_head_toGraph (k : Fin (Fintype.card M.Edge)) : + (M.toGraph.head k).val + = enc (M.pos (M.edgeOf k).1 (M.edgeOf k).2.1 (M.edgeOf k).2.2) := rfl + +end MultiTest + +/-! ### The composed proof's positions -/ + +namespace RegCSP + +open NumEnc BooleanAnalysis Tester + +variable {β : Type} [Fintype β] [DecidableEq β] [Nonempty β] (R : RegCSP β) + [NumEnc R.graph.V] [NumEnc R.graph.D] {B : ℕ} + +omit [Fintype β] [DecidableEq β] [Nonempty β] in +/-- **A dart's number splits into its vertex and its label.** -/ +theorem enc_dart (p : R.Dart) : + NumEnc.enc p = NumEnc.enc p.1 * card R.graph.D + NumEnc.enc p.2 := rfl + +end RegCSP + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgEdge.lean b/Complexitylib/Classes/PCP/Internal/AlgEdge.lean new file mode 100644 index 00000000..b59e83a4 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgEdge.lean @@ -0,0 +1,433 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.PCP.Internal.AlgKeyFn +public import Complexitylib.Classes.PCP.Internal.AlgPosNum + +/-! +# A composed edge's record + +An edge of the composed graph carries three numbers: its two endpoints and the +code of its constraint. The first endpoint is arithmetic in the edge number; the +second is a position, laid out by kind — an encoding block, a linear table or a +quadratic one — and the third is the code the tester's verdict names. + +This module writes the position's layout out as an algorithm. + +## Main definitions + +- `Complexity.posBlk` — a position's number, from its kind, block and cube +- `Complexity.tailBlk` — an edge's first endpoint +- `Complexity.kindBlk`, `Complexity.blockBlk` — which kind of block the second + endpoint lies in, and which block +- `Complexity.headBlk` — the second endpoint itself +- `Complexity.edgeRule`, `Complexity.stepFn` — one edge's record, and the graph + a round produces + +## Main results + +- `Complexity.posBlk_eq` — it is `RegCSP.posNum` +- `Complexity.posBlk_mem_FP` — and it is computed in polynomial time +- `Complexity.tailBlk_eq`, `Complexity.tailBlk_mem_FP` — likewise for the first + endpoint +-/ + +@[expose] public section + +namespace Complexity + +open BooleanAnalysis Tester + +/-- A position's number, from its kind, the block it lies in and the cube inside +that block. Kind `0` is an encoding block, kind `1` a dart's linear table, and +anything else a dart's quadratic table. -/ +noncomputable def posBlk (cardB cardN cardNN cardD : ℕ) + (cardV kind block cube : List Bool) : List Bool := + ifEqLen kind [] (marks (mulC cardB block) ++ cube) + (ifEqLen kind [true] + (marks (mulC cardB cardV) ++ (marks (mulC cardN block) ++ cube)) + (marks (mulC cardB cardV) + ++ (marks (mulC (cardD * cardN) cardV) ++ (marks (mulC cardNN block) ++ cube)))) + +/-- **The layout computes the position's number.** -/ +theorem posBlk_eq (cardB cardN cardNN cardD k b c : ℕ) (cardV kind block cube : List Bool) + (hk : kind.length = k) (hb : block.length = b) (hc : cube = List.replicate c true) : + posBlk cardB cardN cardNN cardD cardV kind block cube + = List.replicate (RegCSP.posNum cardV.length cardD cardB cardN cardNN k b c) true := by + have happ : ∀ m n : ℕ, List.replicate m true ++ List.replicate n true + = List.replicate (m + n) true := fun m n => (List.replicate_add m n true).symm + rw [posBlk, RegCSP.posNum] + by_cases h0 : k = 0 + · rw [ifEqLen_pos (by simp [hk, h0]), if_pos h0, marks_eq, length_mulC, hb, hc, happ] + rw [ifEqLen_neg (by simp [hk, h0]), if_neg h0] + by_cases h1 : k = 1 + · rw [ifEqLen_pos (by simp [hk, h1]), if_pos h1, marks_eq, marks_eq, length_mulC, + length_mulC, hb, hc, happ, happ] + rw [ifEqLen_neg (by simp [hk, h1]), if_neg h1, marks_eq, marks_eq, marks_eq, + length_mulC, length_mulC, length_mulC, hb, hc, happ, happ, happ] + congr 2 + ring + +theorem posBlk_mem_FP {cardB cardN cardNN cardD : ℕ} + {cardV kind block cube : List Bool → List Bool} + (hV : cardV ∈ FP) (hk : kind ∈ FP) (hb : block ∈ FP) (hc : cube ∈ FP) : + (fun w => posBlk cardB cardN cardNN cardD (cardV w) (kind w) (block w) (cube w)) ∈ FP := by + have hbb := marks_mem_FP (mulC_mem_FP hb cardB) + have hVB := marks_mem_FP (mulC_mem_FP hV cardB) + have hNb := marks_mem_FP (mulC_mem_FP hb cardN) + have hNV := marks_mem_FP (mulC_mem_FP hV (cardD * cardN)) + have hNNb := marks_mem_FP (mulC_mem_FP hb cardNN) + exact ifEqLen_mem_FP hk (constFn_mem_FP []) (Cobham.appendFn_mem_FP hbb hc) + (ifEqLen_mem_FP hk (constFn_mem_FP [true]) + (Cobham.appendFn_mem_FP hVB (Cobham.appendFn_mem_FP hNb hc)) + (Cobham.appendFn_mem_FP hVB + (Cobham.appendFn_mem_FP hNV (Cobham.appendFn_mem_FP hNNb hc)))) + +/-! ### The first endpoint -/ + +variable {α : Type} [Fintype α] [DecidableEq α] + +/-- An edge's first endpoint: the test vertex it belongs to, after all the +positions. -/ +noncomputable def tailBlk (posF : ℕ) (r : Round) (w : List Bool) : List Bool := + marks (mulC posF (posCount (pairSnd (pairFst w)))) + ++ (marks (mulC r.cZ (testFn r w)) ++ randFn r w) + +theorem tailBlk_mem_FP (posF : ℕ) (r : Round) : tailBlk posF r ∈ FP := + Cobham.appendFn_mem_FP + (marks_mem_FP (mulC_mem_FP (posCount_mem_FP + (mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP)) posF)) + (Cobham.appendFn_mem_FP (marks_mem_FP (mulC_mem_FP (testFn_mem_FP r) r.cZ)) + (randFn_mem_FP r)) + +/-- **The first endpoint's algorithm computes it.** -/ +theorem tailBlk_eq (posF : ℕ) (r : Round) (G : ConstraintGraph α) {w : List Bool} {t zN : ℕ} + (hg : pairFst w = encGraph G) (ht : testFn r w = List.replicate t true) + (hz : randFn r w = List.replicate zN true) : + tailBlk posF r w = List.replicate (G.numEdges * posF + (t * r.cZ + zN)) true := by + have hcnt : (posCount (pairSnd (encGraph G))).length = G.numEdges := by + have h := gEdges_encGraph G + rwa [gEdges] at h + rw [tailBlk, hg, ht, hz, marks_eq, marks_eq, length_mulC, length_mulC, hcnt, + List.length_replicate, ← List.replicate_add, ← List.replicate_add] + +/-! ### Which block the second endpoint lies in -/ + +theorem readFn_length_le (w : List Bool) : (readFn w).length ≤ 22 := by + rw [readFn, modC_eq (by omega), List.length_replicate] + exact le_of_lt (Nat.mod_lt _ (by omega)) + +/-- The kind of block a read lands in. -/ +noncomputable def kindBlk (w : List Bool) : List Bool := + List.replicate (RegCSP.readKind (decOr ReadIdx.f1x (readFn w).length)) true + +theorem kindBlk_mem_FP : kindBlk ∈ FP := + mem_FP_of_bounded_key readFn_mem_FP readFn_length_le + (fun s => List.replicate (RegCSP.readKind (decOr ReadIdx.f1x s.length)) true) + +/-- **The kind is the read's.** -/ +theorem kindBlk_eq {w : List Bool} {i : ReadIdx} (hi : readFn w = List.replicate + (NumEnc.enc i) true) : + kindBlk w = List.replicate (RegCSP.readKind i) true := by + rw [kindBlk, hi, List.length_replicate, decOr_enc] + +/-- The block the second endpoint lies in: the dart's tail for the first input +read, its head for the second, and the dart itself otherwise. -/ +noncomputable def blockBlk (F : FinBase) (pol : Polynomial ℕ) (r : Round) (w : List Bool) : + List Bool := + ifEqLen (readFn w) (List.replicate (NumEnc.enc ReadIdx.i5r) true) (vertFn r w) + (ifEqLen (readFn w) (List.replicate (NumEnc.enc ReadIdx.i6r) true) + (pairFst (killedRotFn F pol r.deg r.P r.T r.q (killArg r w))) + (testFn r w)) + +theorem blockBlk_mem_FP (F : FinBase) (pol : Polynomial ℕ) (r : Round) : + blockBlk F pol r ∈ FP := by + have hrot : (fun w : List Bool => + pairFst (killedRotFn F pol r.deg r.P r.T r.q (killArg r w))) ∈ FP := + mem_FP_of_eq (mem_FP_comp (killArg_mem_FP r) + (mem_FP_comp (killedRotFn_mem_FP F pol r.deg r.P r.T r.q) Cobham.fstBlock_mem_FP)) + fun _ => rfl + exact ifEqLen_mem_FP readFn_mem_FP (constFn_mem_FP _) (vertFn_mem_FP r) + (ifEqLen_mem_FP readFn_mem_FP (constFn_mem_FP _) hrot (testFn_mem_FP r)) + +/-- **The block is the one the read asks for.** -/ +theorem blockBlk_eq {β : Type} [Fintype β] [DecidableEq β] [Nonempty β] {R : RegCSP β} + [NumEnc R.graph.V] [NumEnc R.graph.D] (F : FinBase) (pol : Polynomial ℕ) (r : Round) + {w : List Bool} (p : R.Dart) (i : ReadIdx) + (hread : readFn w = List.replicate (NumEnc.enc i) true) + (hv : vertFn r w = List.replicate (NumEnc.enc p.1) true) + (ht : testFn r w = List.replicate (NumEnc.enc p) true) + (hrot : pairFst (killedRotFn F pol r.deg r.P r.T r.q (killArg r w)) + = List.replicate (NumEnc.enc (R.graph.rot p).1) true) : + blockBlk F pol r w = List.replicate (R.blockNum p i) true := by + have hne : ∀ j k : ReadIdx, j ≠ k → NumEnc.enc j ≠ NumEnc.enc k := + fun j k h hcon => h (NumEnc.enc_injective hcon) + rw [blockBlk, hread] + rcases eq_or_ne i ReadIdx.i5r with rfl | h5 + · rw [ifEqLen_pos (by simp), hv, RegCSP.blockNum] + rw [ifEqLen_neg (by simpa using hne i ReadIdx.i5r h5)] + rcases eq_or_ne i ReadIdx.i6r with rfl | h6 + · rw [ifEqLen_pos (by simp), hrot, RegCSP.blockNum] + rfl + rw [ifEqLen_neg (by simpa using hne i ReadIdx.i6r h6), ht] + cases i <;> first | rfl | exact absurd rfl h5 | exact absurd rfl h6 + +/-! ### The second endpoint -/ + +/-- How many vertices the powered graph has: twice the input's edge count. -/ +noncomputable def vertCount (w : List Bool) : List Bool := + marks (mulC 2 (posCount (pairSnd (pairFst w)))) + +theorem vertCount_mem_FP : vertCount ∈ FP := + marks_mem_FP (mulC_mem_FP (posCount_mem_FP + (mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP)) 2) + +theorem vertCount_eq (G : ConstraintGraph α) {w : List Bool} + (hg : pairFst w = encGraph G) : + vertCount w = List.replicate (2 * G.numEdges) true := by + have hcnt : (posCount (pairSnd (encGraph G))).length = G.numEdges := by + have h := gEdges_encGraph G + rwa [gEdges] at h + rw [vertCount, hg, marks_eq, length_mulC, hcnt, Nat.mul_comm] + +/-- An edge's second endpoint. -/ +noncomputable def headBlk (F : FinBase) (pol : Polynomial ℕ) (r : Round) + (cardB cardN cardNN : ℕ) {E : ExpanderFamily} {B : ℕ} + (dflt : StepKey E r.T r.q B (Fintype.card (α → α → Bool))) + (encβ : (PreWalk E r.T → α) → Cube B) (w : List Bool) : List Bool := + posBlk cardB cardN cardNN r.cD (vertCount w) (kindBlk w) (blockBlk F pol r w) + (cubeFn F pol r dflt encβ w) + +theorem headBlk_mem_FP (F : FinBase) (pol : Polynomial ℕ) (r : Round) + (cardB cardN cardNN : ℕ) {E : ExpanderFamily} {B : ℕ} + (hQ : 0 < r.cQ) (hD : 0 < r.cD) (hZ : 0 < r.cZ) (hC : 0 < r.C) + (dflt : StepKey E r.T r.q B (Fintype.card (α → α → Bool))) + (encβ : (PreWalk E r.T → α) → Cube B) : + headBlk F pol r cardB cardN cardNN dflt encβ ∈ FP := + posBlk_mem_FP vertCount_mem_FP kindBlk_mem_FP (blockBlk_mem_FP F pol r) + (cubeFn_mem_FP F pol r hQ hD hZ hC dflt encβ) + +/-- **The second endpoint's algorithm computes its number.** -/ +theorem headBlk_eq (F : FinBase) (pol : Polynomial ℕ) (r : Round) + (cardB cardN cardNN : ℕ) {E : ExpanderFamily} {B : ℕ} + (dflt : StepKey E r.T r.q B (Fintype.card (α → α → Bool))) + (encβ : (PreWalk E r.T → α) → Cube B) {w : List Bool} {V k b c : ℕ} + (hV : vertCount w = List.replicate V true) + (hk : kindBlk w = List.replicate k true) + (hb : blockBlk F pol r w = List.replicate b true) + (hc : cubeFn F pol r dflt encβ w = List.replicate c true) : + headBlk F pol r cardB cardN cardNN dflt encβ w + = List.replicate (RegCSP.posNum V r.cD cardB cardN cardNN k b c) true := by + have hVlen : (vertCount w).length = V := by rw [hV, List.length_replicate] + rw [headBlk, posBlk_eq cardB cardN cardNN r.cD k b c _ _ _ _ + (by rw [hk, List.length_replicate]) (by rw [hb, List.length_replicate]) hc, hVlen] + +/-! ### The label encoding, without the graph -/ + +theorem card_preDart (E : ExpanderFamily) : Fintype.card (PreDart E) = Dinur.powDeg E := by + show Fintype.card (Unit ⊕ (Option (Fin E.degree) ⊕ Fin E.degree)) = _ + rw [Dinur.powDeg] + simp + omega + +theorem card_preWalk (E : ExpanderFamily) (T : ℕ) : + Fintype.card (PreWalk E T) = Dinur.walkCount E T := by + rw [Dinur.walkCount, ← Fin.sum_univ_eq_sum_range (fun ℓ => Dinur.powDeg E ^ ℓ) (T + 1)] + show Fintype.card (Σ ℓ : Fin (T + 1), Fin ℓ.val → PreDart E) = _ + rw [Fintype.card_sigma] + refine Finset.sum_congr rfl fun ℓ _ => ?_ + rw [Fintype.card_fun, Fintype.card_fin, card_preDart] + +theorem card_preOpinion (E : ExpanderFamily) (T : ℕ) : + Fintype.card (PreWalk E T → DinurAlpha) = Dinur.bits E T := by + rw [Fintype.card_fun, card_preWalk, Dinur.bits] + +/-! ### The round's constants -/ + +/-- The constants of a Dinur round at killing rate `q`, over the expander family +a finite base generates. The alphabet's constraint count and the tester's string +count are supplied, so that they carry the caller's own instances. -/ +noncomputable def dinurRound (F : FinBase) (hd : 1 < F.deg) (q C cZ : ℕ) : Round where + deg := (F.toFamily hd).degree + P := 2 + 2 * (F.toFamily hd).degree + T := powT Dinur.K q + q := q + C := C + cZ := cZ + +@[simp] theorem dinurRound_deg (F : FinBase) (hd : 1 < F.deg) (q C cZ : ℕ) : + (dinurRound F hd q C cZ).deg = (F.toFamily hd).degree := rfl + +theorem dinurRound_P (F : FinBase) (hd : 1 < F.deg) (q C cZ : ℕ) + (G : ConstraintGraph DinurAlpha) : + (dinurRound F hd q C cZ).P = G.preDeg (F.toFamily hd) := + (G.preDeg_eq (F.toFamily hd)).symm + +@[simp] theorem dinurRound_T (F : FinBase) (hd : 1 < F.deg) (q C cZ : ℕ) : + (dinurRound F hd q C cZ).T = powT Dinur.K q := rfl + +@[simp] theorem dinurRound_q (F : FinBase) (hd : 1 < F.deg) (q C cZ : ℕ) : + (dinurRound F hd q C cZ).q = q := rfl + +@[simp] theorem dinurRound_C (F : FinBase) (hd : 1 < F.deg) (q C cZ : ℕ) : + (dinurRound F hd q C cZ).C = C := rfl + +@[simp] theorem dinurRound_cZ (F : FinBase) (hd : 1 < F.deg) (q C cZ : ℕ) : + (dinurRound F hd q C cZ).cZ = cZ := rfl + +@[simp] theorem dinurRound_cQ (F : FinBase) (hd : 1 < F.deg) (q C cZ : ℕ) : + (dinurRound F hd q C cZ).cQ = q ^ powT Dinur.K q := rfl + +/-! ### The round's output -/ + +/-- One edge's record: its two endpoints and the code of its constraint. -/ +noncomputable def edgeRule (F : FinBase) (pol : Polynomial ℕ) (r : Round) + (posF cardB cardN cardNN : ℕ) {E : ExpanderFamily} {B : ℕ} + (dflt : StepKey E r.T r.q B (Fintype.card (α → α → Bool))) + (encβ : (PreWalk E r.T → α) → Cube B) (w : List Bool) : List Bool := + encTriple (tailBlk posF r w) (headBlk F pol r cardB cardN cardNN dflt encβ w) + (codeFn F pol r dflt encβ w) + +theorem edgeRule_mem_FP (F : FinBase) (pol : Polynomial ℕ) (r : Round) + (posF cardB cardN cardNN : ℕ) {E : ExpanderFamily} {B : ℕ} + (hQ : 0 < r.cQ) (hD : 0 < r.cD) (hZ : 0 < r.cZ) (hC : 0 < r.C) + (dflt : StepKey E r.T r.q B (Fintype.card (α → α → Bool))) + (encβ : (PreWalk E r.T → α) → Cube B) : + edgeRule F pol r posF cardB cardN cardNN dflt encβ ∈ FP := + encTriple_mem_FP (tailBlk_mem_FP posF r) + (headBlk_mem_FP F pol r cardB cardN cardNN hQ hD hZ hC dflt encβ) + (codeFn_mem_FP F pol r hQ hD hZ hC dflt encβ) + +/-- **The graph a round produces**, from the graph it is given. -/ +noncomputable def stepFn (F : FinBase) (pol : Polynomial ℕ) (r : Round) + (vertF edgeF posF cardB cardN cardNN : ℕ) {E : ExpanderFamily} {B : ℕ} + (dflt : StepKey E r.T r.q B (Fintype.card (α → α → Bool))) + (encβ : (PreWalk E r.T → α) → Cube B) : List Bool → List Bool := + buildGraph (fun z => marks (mulC vertF (posCount (pairSnd z)))) + (fun z => marks (mulC edgeF (posCount (pairSnd z)))) + (edgeRule F pol r posF cardB cardN cardNN dflt encβ) + +/-- **A count block is a constant multiple of the input's edge count.** -/ +theorem countBlk_eq (G : ConstraintGraph α) (c : ℕ) : + marks (mulC c (posCount (pairSnd (encGraph G)))) + = List.replicate (c * G.numEdges) true := by + have hcnt : (posCount (pairSnd (encGraph G))).length = G.numEdges := by + have h := gEdges_encGraph G + rwa [gEdges] at h + rw [marks_eq, length_mulC, hcnt, Nat.mul_comm] + +theorem stepFn_mem_FP (F : FinBase) (pol : Polynomial ℕ) (r : Round) + (vertF edgeF posF cardB cardN cardNN : ℕ) {E : ExpanderFamily} {B : ℕ} + (hQ : 0 < r.cQ) (hD : 0 < r.cD) (hZ : 0 < r.cZ) (hC : 0 < r.C) + (dflt : StepKey E r.T r.q B (Fintype.card (α → α → Bool))) + (encβ : (PreWalk E r.T → α) → Cube B) : + stepFn F pol r vertF edgeF posF cardB cardN cardNN dflt encβ ∈ FP := + buildGraph_mem_FP (marks_mem_FP (mulC_mem_FP (posCount_mem_FP Cobham.sndBlock_mem_FP) vertF)) + (marks_mem_FP (mulC_mem_FP (posCount_mem_FP Cobham.sndBlock_mem_FP) edgeF)) + (edgeRule_mem_FP F pol r posF cardB cardN cardNN hQ hD hZ hC dflt encβ) + +/-- **One round of amplification, computed.** The algorithm's output is the +graph the round produces. -/ +theorem stepFn_eq (F : FinBase) (pol : Polynomial ℕ) (hd : 1 < F.deg) + (G : ConstraintGraph DinurAlpha) + (r : Round) (hq : 0 < r.q) (vertF edgeF posF cardB cardN cardNN : ℕ) + (hrD : r.cD = NumEnc.card ((G.preprocess (F.toFamily hd)).killedPow r.q r.T hq).graph.D) + (hrZ : r.cZ = 2 ^ ROf (Dinur.bits (F.toFamily hd) r.T)) + (hdeg : r.deg = (F.toFamily hd).degree) (hP : r.P = G.preDeg (F.toFamily hd)) + (hC : r.C = Fintype.card (DinurAlpha → DinurAlpha → Bool)) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) + (hvertF : (((G.preprocess (F.toFamily hd)).killedPow r.q r.T hq).compose + (Dinur.enc (F.toFamily hd) G r.T)).toGraph.numVerts = vertF * G.numEdges) + (hedgeF : (((G.preprocess (F.toFamily hd)).killedPow r.q r.T hq).compose + (Dinur.enc (F.toFamily hd) G r.T)).toGraph.numEdges = edgeF * G.numEdges) + (hcardB : cardB = NumEnc.card (Cube (Dinur.bits (F.toFamily hd) r.T))) + (hcardN : cardN = NumEnc.card (Cube (nOf (Dinur.bits (F.toFamily hd) r.T)))) + (hcardNN : cardNN = NumEnc.card (Cube (nOf (Dinur.bits (F.toFamily hd) r.T) + * nOf (Dinur.bits (F.toFamily hd) r.T)))) + (hposF : Fintype.card (((G.preprocess (F.toFamily hd)).killedPow r.q r.T hq).Pos + (B := Dinur.bits (F.toFamily hd) r.T)) = G.numEdges * posF) + (dflt : StepKey (F.toFamily hd) r.T r.q (Dinur.bits (F.toFamily hd) r.T) + (Fintype.card (DinurAlpha → DinurAlpha → Bool))) + : + stepFn F pol r vertF edgeF posF cardB cardN cardNN dflt + (Dinur.enc (F.toFamily hd) G r.T) (encGraph G) + = encGraph ((((G.preprocess (F.toFamily hd)).killedPow r.q r.T hq).compose + (Dinur.enc (F.toFamily hd) G r.T)).toGraph) := by + refine buildGraph_eq ?_ ?_ ?_ + · rw [countBlk_eq, hvertF] + · rw [countBlk_eq, hedgeF] + · intro e he + obtain ⟨p, z, i, hsplit, htail, hhead, hrel⟩ := RegCSP.edge_facts + ((G.preprocess (F.toFamily hd)).killedPow r.q r.T hq) + (Dinur.enc (F.toFamily hd) G r.T) e he + rw [htail, hhead, hrel] + rw [← hrD, ← hrZ] at hsplit + have hDpos : 0 < r.cD := by + rw [hrD] + have := NumEnc.enc_lt p.2 + omega + have hblt : NumEnc.enc p.2 < r.cD := by + rw [hrD] + exact NumEnc.enc_lt p.2 + have hilt : NumEnc.enc i < 22 := NumEnc.enc_lt i + have hclt : NumEnc.enc z < r.cZ := by + rw [hrZ] + have h := NumEnc.enc_lt z + rw [NumEnc.card_eq_fintype_card, card_cube] at h + exact h + subst hsplit + have hV' : NumEnc.card ((G.preprocess (F.toFamily hd)).killedPow r.q r.T hq).graph.V + = 2 * G.numEdges := by + rw [NumEnc.card_eq_fintype_card] + exact G.order_preprocess (F.toFamily hd) + obtain ⟨htest, hvert, hdart, hrand, hread⟩ := + blocks_eq r hDpos (by omega) (encGraph G) + (NumEnc.enc p.1) (NumEnc.enc p.2) (NumEnc.enc z) (NumEnc.enc i) hblt hclt hilt + rw [edgeRule, tailBlk_eq posF r G (pairFst_pair _ _) htest hrand, + RegCSP.tailNum_split' (cZ := r.cZ) _ _ _ _ _ _ hrZ hclt hilt, hposF] + rw [codeFn_eq' (B := Dinur.bits (F.toFamily hd) r.T) r hd G hq hdeg hP hC (by omega) p z i + (by rw [hrZ, NumEnc.card_eq_fintype_card, card_cube]) hpc hpe dflt + (Dinur.enc (F.toFamily hd) G r.T)] + have hread : readFn (pair (encGraph G) (List.replicate + (((NumEnc.enc p.1 * r.cD + NumEnc.enc p.2) * r.cZ + NumEnc.enc z) * 22 + + NumEnc.enc i) true)) = List.replicate (NumEnc.enc i) true := hread + have hcube := cubeFn_eq' (B := Dinur.bits (F.toFamily hd) r.T) r hd G hq hdeg hP hC + (by omega) p z i (by rw [hrZ, NumEnc.card_eq_fintype_card, card_cube]) hpc hpe dflt + (Dinur.enc (F.toFamily hd) G r.T) + have hblock : blockBlk F pol r (pair (encGraph G) (List.replicate + (((NumEnc.enc p.1 * r.cD + NumEnc.enc p.2) * r.cZ + NumEnc.enc z) * 22 + + NumEnc.enc i) true)) + = List.replicate (((G.preprocess (F.toFamily hd)).killedPow r.q r.T hq).blockNum p i) + true := by + refine blockBlk_eq F pol r p i hread hvert ?_ ?_ + · have hdart' : NumEnc.enc p = NumEnc.enc p.1 * r.cD + NumEnc.enc p.2 := by + rw [RegCSP.enc_dart, hrD] + rw [hdart'] + exact htest + · have hv2 : NumEnc.enc p.1 < 2 * G.numEdges := by + have h := NumEnc.enc_lt p.1 + rwa [hV'] at h + have hc2 : NumEnc.enc p.2.2 < r.q ^ r.T := NumEnc.enc_lt p.2.2 + have hd2 : NumEnc.enc p.2 = NumEnc.enc p.2.1 * r.q ^ r.T + NumEnc.enc p.2.2 := rfl + have hkr := killedRotFn_eq hd G r.T r.q (NumEnc.enc p.1) (NumEnc.enc p.2.1) + (NumEnc.enc p.2.2) hq hv2 hc2 hpc hpe + rw [killArg_eq r (pairFst_pair _ _) hvert hdart, hdeg, hP, hd2, hkr, + pairFst_pair] + have hrn := G.killedRotNum_eq (F.toFamily hd) hq (G.preDeg_pos _) p.1 p.2 + (w := (((G.preprocess (F.toFamily hd)).graph.killedPower r.q r.T hq).rot + (p.1, p.2)).1) + (y := (((G.preprocess (F.toFamily hd)).graph.killedPower r.q r.T hq).rot + (p.1, p.2)).2) rfl + exact congrArg (fun n => List.replicate n true) (congrArg Prod.fst hrn) + rw [headBlk_eq F pol r cardB cardN cardNN dflt (Dinur.enc (F.toFamily hd) G r.T) + (vertCount_eq G (pairFst_pair _ _)) (kindBlk_eq hread) hblock hcube] + rw [hV', ← hrD, ← hcardB, ← hcardN, ← hcardNN] + rfl + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgFamily.lean b/Complexitylib/Classes/PCP/Internal/AlgFamily.lean new file mode 100644 index 00000000..d7c631ad --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgFamily.lean @@ -0,0 +1,255 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.PCP.Internal.AlgLevel +public import Complexitylib.Classes.PCP.Internal.TowerTable +public import Complexitylib.Classes.PCP.Internal.FamilyFin + +/-! +# The expander's table, for a requested size + +`AlgLevel` finds the tower level a request calls for, and `TowerTable` writes the +rotation table of a level. Putting the two together writes the table of the +level a request calls for — and it is polynomially long, because the level the +search reports always names a size within a constant factor of the request. + +## Main definitions + +- `Complexity.FinBase.famTableFn` — the table for a requested size + +## Main results + +- `Complexity.FinBase.famTableFn_mem_FP` — writing it is polynomial time +- `Complexity.FinBase.famRotFn_mem_FP` — and the family's rotation map is + polynomial time +- `Complexity.FinBase.levelFn_fitLevel` — the search reports the level the + family uses +-/ + +@[expose] public section + +namespace Complexity + +namespace FinBase + +variable (F : FinBase) (p : Polynomial ℕ) + +/-- The level the search reports for a request. -/ +noncomputable def searchLevel (z : List Bool) : ℕ := (levelFn (F.deg ^ 4) p z).length + +/-- **The size at the reported level is within a constant factor of the +request.** -/ +theorem size_searchLevel_le (z : List Bool) : + F.size (F.searchLevel p z) ≤ F.deg ^ 4 + 2 * z.length * F.deg ^ 4 := + pow_levelFn_le (F.deg ^ 4) p z + +theorem size_le_of_le {l : ℕ} (z : List Bool) (hl : l ≤ F.searchLevel p z) : + F.size l ≤ F.deg ^ 4 + 2 * z.length * F.deg ^ 4 := by + refine le_trans ?_ (F.size_searchLevel_le p z) + rw [size, size] + have hbase : 1 ≤ F.deg ^ 4 := pow_pos F.deg_pos 4 + exact Nat.pow_le_pow_right hbase (by omega) + +/-- The table of the level a request calls for. -/ +noncomputable def famTableFn (z : List Bool) : List Bool := F.table (F.searchLevel p z) + +/-- The polynomial that bounds that table. -/ +noncomputable def tableWidth : Polynomial ℕ := + Polynomial.C 2 + + (Polynomial.C (F.deg ^ 4) + Polynomial.C (2 * F.deg ^ 4) * Polynomial.X) + * Polynomial.C (F.deg ^ 2) + * (Polynomial.C 4 * (Polynomial.C (F.deg ^ 4) + + Polynomial.C (2 * F.deg ^ 4) * Polynomial.X) + + Polynomial.C (4 * F.deg ^ 2 + 6)) + +theorem eval_tableWidth (n : ℕ) : + F.tableWidth.eval n + = 2 + (F.deg ^ 4 + 2 * F.deg ^ 4 * n) * F.deg ^ 2 + * (4 * (F.deg ^ 4 + 2 * F.deg ^ 4 * n) + (4 * F.deg ^ 2 + 6)) := by + simp only [tableWidth, Polynomial.eval_add, Polynomial.eval_mul, Polynomial.eval_C, + Polynomial.eval_X] + +/-- **Writing the table for a requested size is polynomial time.** -/ +theorem famTableFn_mem_FP : F.famTableFn p ∈ FP := by + refine F.table_mem_FP (levelFn_mem_FP (F.deg ^ 4) p) + (polyRulerFn_mem_FP F.tableWidth id_mem_FP) ?_ + intro z l hl + rw [polyRuler_length, eval_tableWidth] + refine le_trans (F.length_table_le l) ?_ + have hsize : F.size l ≤ F.deg ^ 4 + 2 * z.length * F.deg ^ 4 := + F.size_le_of_le p z hl + have hM : F.deg ^ 4 + 2 * z.length * F.deg ^ 4 = F.deg ^ 4 + 2 * F.deg ^ 4 * z.length := by + ring + rw [hM] at hsize + have h1 : F.size l * F.deg ^ 2 ≤ (F.deg ^ 4 + 2 * F.deg ^ 4 * z.length) * F.deg ^ 2 := + Nat.mul_le_mul_right _ hsize + have h2 : 4 * F.size l + 4 * F.deg ^ 2 + 6 + ≤ 4 * (F.deg ^ 4 + 2 * F.deg ^ 4 * z.length) + (4 * F.deg ^ 2 + 6) := by omega + exact Nat.add_le_add_left (Nat.mul_le_mul h1 h2) 2 + +/-! ### The level the family asks for -/ + +/-- **The search reports the level the family uses.** -/ +theorem levelFn_fitLevel (hd : 1 < F.deg) (n : ℕ) + (hp : F.fitLevel hd n ≤ p.eval n) : + (levelFn (F.deg ^ 4) p (List.replicate n true)).length = F.fitLevel hd n := by + have hlen : (List.replicate n true).length = n := List.length_replicate + refine levelFn_length _ p _ _ ?_ ?_ (by rw [hlen]; exact hp) + · rw [hlen] + have := F.le_size_level hd (2 * n) + rw [size] at this + exact this + · intro i hi + rw [hlen] + have hmin := Nat.find_min (F.exists_size_ge hd (2 * n)) (m := i) hi + rw [size] at hmin + exact hmin + +/-- **The search reports the size the family uses.** -/ +theorem sizeFn_fitN (hd : 1 < F.deg) (n : ℕ) (hp : F.fitLevel hd n ≤ p.eval n) : + (sizeFn (F.deg ^ 4) p (List.replicate n true)).length = F.fitN hd n := by + rw [sizeFn_length, F.levelFn_fitLevel p hd n hp, fitN, size] + +/-- **And writes that level's table.** -/ +theorem famTableFn_eq (hd : 1 < F.deg) (n : ℕ) (hp : F.fitLevel hd n ≤ p.eval n) : + F.famTableFn p (List.replicate n true) = F.table (F.fitLevel hd n) := by + rw [famTableFn, searchLevel, F.levelFn_fitLevel p hd n hp] + +/-! ### The family's rotation map -/ + +/-- The family's rotation map, on `pair (unary n) (pair (unary v) (unary i))`: +split the dart into a slot and a step, lift the vertex into the tower member, +look the step up in that member's table, and fold the answer back onto `n` +vertices. Darts past the fold's degree are self-loops. -/ +noncomputable def famRotFn (z : List Bool) : List Bool := + let n := pairFst z + let v := pairFst (pairSnd z) + let i := pairSnd (pairSnd z) + let N := sizeFn (F.deg ^ 4) p n + let T := F.famTableFn p n + let m := divFn2 (pair n N) ++ [true] + let s := divC F.fitD i + let c := modC F.fitD i + let lift := v ++ mulLen s n + let y1 := tableFst T (mulLen lift (List.replicate F.fitD true) ++ c).length + let y2 := tableSnd T (mulLen lift (List.replicate F.fitD true) ++ c).length + ifLtLen i (mulLen m (List.replicate F.fitD true)) + (ifLtLen lift N + (pair (marks (modFn2 (pair n y1))) + (marks (y2 ++ mulC F.fitD (divFn2 (pair n y1))))) + (pair (marks v) (marks i))) + (pair (marks v) (marks i)) + +theorem famRotFn_mem_FP : F.famRotFn p ∈ FP := by + have hn : (fun z : List Bool => pairFst z) ∈ FP := Cobham.fstBlock_mem_FP + have hv : (fun z : List Bool => pairFst (pairSnd z)) ∈ FP := + mem_FP_of_eq (mem_FP_comp Cobham.sndBlock_mem_FP Cobham.fstBlock_mem_FP) fun _ => rfl + have hi : (fun z : List Bool => pairSnd (pairSnd z)) ∈ FP := + mem_FP_of_eq (mem_FP_comp Cobham.sndBlock_mem_FP Cobham.sndBlock_mem_FP) fun _ => rfl + have hN : (fun z : List Bool => sizeFn (F.deg ^ 4) p (pairFst z)) ∈ FP := + mem_FP_of_eq (mem_FP_comp hn (sizeFn_mem_FP _ p)) fun _ => rfl + have hT : (fun z : List Bool => F.famTableFn p (pairFst z)) ∈ FP := + mem_FP_of_eq (mem_FP_comp hn (F.famTableFn_mem_FP p)) fun _ => rfl + have hm := Cobham.appendFn_mem_FP + (mem_FP_of_eq (mem_FP_comp (Cobham.pairFn_mem_FP hn hN) divFn2_mem_FP) fun _ => rfl) + (constFn_mem_FP [true]) + have hs := divC_mem_FP hi F.fitD + have hc := modC_mem_FP hi F.fitD + have hlift := Cobham.appendFn_mem_FP hv (mulLen_mem_FP hs hn) + have hidx := Cobham.appendFn_mem_FP + (mulLen_mem_FP hlift (constFn_mem_FP (List.replicate F.fitD true))) hc + have hy1 := tableFst_mem_FP hidx hT + have hy2 := tableSnd_mem_FP hidx hT + have hmod := mem_FP_of_eq + (mem_FP_comp (Cobham.pairFn_mem_FP hn hy1) modFn2_mem_FP) fun _ => rfl + have hdiv := mem_FP_of_eq + (mem_FP_comp (Cobham.pairFn_mem_FP hn hy1) divFn2_mem_FP) fun _ => rfl + have hstep := Cobham.pairFn_mem_FP (marks_mem_FP hmod) + (marks_mem_FP (Cobham.appendFn_mem_FP hy2 (mulC_mem_FP hdiv F.fitD))) + have hstay := Cobham.pairFn_mem_FP (marks_mem_FP hv) (marks_mem_FP hi) + have hinner := ifLtLen_mem_FP hlift hN hstep hstay + have houter := ifLtLen_mem_FP hi + (mulLen_mem_FP hm (constFn_mem_FP (List.replicate F.fitD true))) hinner hstay + exact mem_FP_of_eq houter fun _ => rfl + +/-- **The rotation function runs the family's rotation map.** -/ +theorem famRotFn_eq (hd : 1 < F.deg) (n v i : ℕ) (hn : 0 < n) + (hp : F.fitLevel hd n ≤ p.eval n) : + F.famRotFn p (pair (List.replicate n true) + (pair (List.replicate v true) (List.replicate i true))) + = pair (List.replicate (F.famRotVal hd n (v, i)).1 true) + (List.replicate (F.famRotVal hd n (v, i)).2 true) := by + have hrep : (List.replicate n true).length = n := List.length_replicate + have hdpos : 0 < F.fitD := F.fitD_pos + have hN : (sizeFn (F.deg ^ 4) p (List.replicate n true)).length = F.fitN hd n := + F.sizeFn_fitN p hd n hp + have hT : F.famTableFn p (List.replicate n true) = F.table (F.fitLevel hd n) := + F.famTableFn_eq p hd n hp + have hm : (divFn2 (pair (List.replicate n true) + (sizeFn (F.deg ^ 4) p (List.replicate n true))) ++ [true]).length = F.wid hd n := by + rw [divFn2_eq (by rw [hrep]; exact hn), List.length_append, List.length_replicate, + List.length_cons, List.length_nil, hrep, hN, wid, RegGraph.mergeWidth] + have hs : (divC F.fitD (List.replicate i true)) = List.replicate (i / F.fitD) true := by + rw [divC_eq hdpos, List.length_replicate] + have hc : (modC F.fitD (List.replicate i true)) = List.replicate (i % F.fitD) true := by + rw [modC_eq hdpos, List.length_replicate] + have hlift : (List.replicate v true + ++ mulLen (divC F.fitD (List.replicate i true)) (List.replicate n true)).length + = v + i / F.fitD * n := by + rw [List.length_append, List.length_replicate, length_mulLen, hs, hrep, + List.length_replicate] + rw [famRotFn, famRotVal] + simp only [pairFst_pair, pairSnd_pair] + by_cases h1 : i < F.wid hd n * F.fitD + · rw [if_pos h1, ifLtLen_pos (by + rw [List.length_replicate, length_mulLen, hm, List.length_replicate] + exact h1)] + by_cases h2 : v + i / F.fitD * n < F.fitN hd n + · rw [if_pos h2, ifLtLen_pos (by rw [hlift, hN]; exact h2)] + have hidx : (mulLen (List.replicate v true + ++ mulLen (divC F.fitD (List.replicate i true)) (List.replicate n true)) + (List.replicate F.fitD true) ++ modC F.fitD (List.replicate i true)).length + = (v + i / F.fitD * n) * F.fitD + i % F.fitD := by + rw [List.length_append, length_mulLen, hlift, List.length_replicate, hc, + List.length_replicate] + have hclt : i % F.fitD < F.deg ^ 2 := Nat.mod_lt _ hdpos + have hbound : (v + i / F.fitD * n) * F.fitD + i % F.fitD + < F.size (F.fitLevel hd n) * F.deg ^ 2 := by + have hfitN : F.fitN hd n = F.size (F.fitLevel hd n) := rfl + rw [hfitN] at h2 + have hsucc : (v + i / F.fitD * n) + 1 ≤ F.size (F.fitLevel hd n) := h2 + have hclt' : i % F.fitD < F.fitD := Nat.mod_lt _ hdpos + show (v + i / F.fitD * n) * F.fitD + i % F.fitD < F.size (F.fitLevel hd n) * F.fitD + calc (v + i / F.fitD * n) * F.fitD + i % F.fitD + < (v + i / F.fitD * n) * F.fitD + F.fitD := by omega + _ = ((v + i / F.fitD * n) + 1) * F.fitD := by ring + _ ≤ F.size (F.fitLevel hd n) * F.fitD := Nat.mul_le_mul_right _ hsucc + rw [hT, hidx, F.tableFst_table hbound, F.tableSnd_table hbound, + show F.deg ^ 2 = F.fitD from rfl, + mul_add_div_of_lt hdpos (Nat.mod_lt _ hdpos), mul_add_mod_of_lt (Nat.mod_lt _ hdpos)] + refine congrArg₂ pair ?_ ?_ + · rw [marks_eq, modFn2_eq (by rw [hrep]; exact hn), + List.length_replicate, hrep, List.length_replicate] + · rw [marks_eq, List.length_append, List.length_replicate, + length_mulC, divFn2_eq (by rw [hrep]; exact hn), List.length_replicate, hrep, + List.length_replicate] + exact congrArg (List.replicate · true) (by ring) + · rw [if_neg h2, ifLtLen_neg (by rw [hlift, hN]; exact h2)] + refine congrArg₂ pair ?_ ?_ + · rw [marks_eq, List.length_replicate] + · rw [marks_eq, List.length_replicate] + · rw [if_neg h1, ifLtLen_neg (by + rw [List.length_replicate, length_mulLen, hm, List.length_replicate] + exact h1)] + refine congrArg₂ pair ?_ ?_ + · rw [marks_eq, List.length_replicate] + · rw [marks_eq, List.length_replicate] + +end FinBase + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgFormula.lean b/Complexitylib/Classes/PCP/Internal/AlgFormula.lean new file mode 100644 index 00000000..0f498a96 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgFormula.lean @@ -0,0 +1,94 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.SAT.ThreeSAT.Completeness +public import Complexitylib.Classes.PCP.Internal.AlgUniform + +/-! +# Every NP language, as a formula + +Cook--Levin and Tseitin already reduce any `NP` language to encoded 3SAT. What +the gap reduction needs is slightly more: not a string that *lies in* 3SAT, but +the *formula itself*, so that the constraint graph can be built from it. + +The Tseitin reduction is total — malformed inputs go to a fixed unsatisfiable +formula — so the formula is always there to be named: `redCNF` names it, and +`reduction_eq_encode` says the reduction writes exactly its encoding. + +## Main definitions + +- `Complexity.redCNF` — the 3CNF a string reduces to + +## Main results + +- `Complexity.exists_reduction_cnf` — every `NP` language is the satisfiability + of an `FP` family of 3CNFs +-/ + +@[expose] public section + +namespace Complexity + +open SAT SAT.ThreeSAT + +/-- The exact 3-CNF that the total Tseitin reduction produces. -/ +noncomputable def redCNF (z : List Bool) : CNF := + match CNF.decode? z with + | some φ => (CNF.to3Aux (z.length + 1) φ).1 + | none => falseFormula + +theorem reduction_eq_encode (z : List Bool) : reduction z = (redCNF z).encode := by + rw [reduction, redCNF] + cases CNF.decode? z with + | none => rfl + | some φ => rfl + +theorem redCNF_is3CNF (z : List Bool) : (redCNF z).Is3CNF := by + rw [redCNF] + cases CNF.decode? z with + | none => exact falseFormula_is3CNF + | some φ => exact CNF.to3Aux_is3CNF _ φ + +theorem satisfiable_redCNF_iff (z : List Bool) : + (redCNF z).Satisfiable ↔ z ∈ CNFSAT.language := by + cases hdecode : CNF.decode? z with + | none => + have hz : z ∉ CNFSAT.language := by + rw [CNFSAT.mem_language_iff_decode] + rintro ⟨φ, hφ, _⟩ + rw [hdecode] at hφ + exact absurd hφ (by simp) + rw [show redCNF z = falseFormula by rw [redCNF, hdecode]] + simp only [hz, iff_false] + exact falseFormula_not_satisfiable + | some φ => + have hz : z = φ.encode := CNF.decode?_sound hdecode + have hfresh : φ.maxVar < z.length + 1 := by + have hmax := CNF.maxVar_le_encode_length φ + rw [hz] + omega + rw [show redCNF z = (CNF.to3Aux (z.length + 1) φ).1 by rw [redCNF, hdecode], + CNF.to3Aux_satisfiable_iff _ φ hfresh, CNFSAT.mem_language_iff_decode] + constructor + · exact fun h => ⟨φ, hdecode, h⟩ + · rintro ⟨ψ, hψ, hsat⟩ + rw [hdecode] at hψ + exact (Option.some.inj hψ) ▸ hsat + +/-- **Every `NP` language is the satisfiability of an `FP` family of exact +3-CNFs.** -/ +theorem exists_reduction_cnf {L : Language} (hL : L ∈ NP) : + ∃ (E : List Bool → List Bool) (Φ : List Bool → CNF), E ∈ FP + ∧ (∀ x, E x = (Φ x).encode) ∧ (∀ x, (Φ x).Is3CNF) + ∧ (∀ x, x ∈ L ↔ (Φ x).Satisfiable) := by + obtain ⟨f, hf, hiff⟩ := SAT.NPHard_language L hL + refine ⟨fun x => reduction (f x), fun x => redCNF (f x), ?_, fun x => reduction_eq_encode _, + fun x => redCNF_is3CNF _, fun x => ?_⟩ + · exact mem_FP_of_eq (mem_FP_comp hf ThreeSAT.reduction_mem_FP) fun x => rfl + · rw [hiff x, ← satisfiable_redCNF_iff (f x)] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgGap.lean b/Complexitylib/Classes/PCP/Internal/AlgGap.lean new file mode 100644 index 00000000..1b8377f7 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgGap.lean @@ -0,0 +1,164 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.AlgRound +public import Complexitylib.Classes.PCP.Internal.AlgLog + +/-! +# Amplifying, logarithmically many times + +`AlgRound` computes one round. Dinur's theorem runs logarithmically many, so +this module iterates that function and bounds the size of what it writes: each +round multiplies the edge count by a constant, so after `n` rounds the graph is +`edgeFactor ^ n` times as large, and the ruler keeps `n` logarithmic. + +## Main definitions + +- `Complexity.gapFn` — the amplified graph, as a string + +## Main results + +- `Complexity.numEdges_iterStep` — a round's edge count, exactly +- `Complexity.length_encGraph_iterStep_le` — the size of what is written +- `Complexity.gapFn_eq` — it writes the amplifier's iterate +-/ + +@[expose] public section + +set_option maxRecDepth 8000 + +namespace Complexity + +open Dinur Tester + +variable (F : FinBase) (hd : 1 < F.deg) + +/-! ### The sizes of an iterate -/ + +theorem numEdges_iterStep (G : ConstraintGraph DinurAlpha) (n : ℕ) : + ((Dinur.step (F.toFamily hd) (qOf F hd) (qOf_pos F hd))^[n] G).numEdges + = edgeFactor (F.toFamily hd) (qOf F hd) ^ n * G.numEdges := by + induction n with + | zero => simp + | succ n ih => + rw [Function.iterate_succ_apply', Dinur.numEdges_step, ih, pow_succ] + ring + +theorem one_le_edgeFactor : 1 ≤ edgeFactor (F.toFamily hd) (qOf F hd) := by + rw [edgeFactor] + have h1 : 0 < powDeg (F.toFamily hd) ^ powT K (qOf F hd) * qOf F hd ^ powT K (qOf F hd) := + Nat.mul_pos (Nat.pow_pos (by rw [powDeg]; omega)) (Nat.pow_pos (qOf_pos F hd)) + have h2 : 0 < 2 ^ ROf (bits (F.toFamily hd) (powT K (qOf F hd))) := Nat.two_pow_pos _ + have := Nat.mul_pos (Nat.mul_pos (Nat.mul_pos (by omega : 0 < 2) h1) h2) (by omega : 0 < 22) + omega + +theorem numVerts_iterStep_le (G : ConstraintGraph DinurAlpha) (n : ℕ) : + ((Dinur.step (F.toFamily hd) (qOf F hd) (qOf_pos F hd))^[n] G).numVerts + ≤ G.numVerts + vertFactor (F.toFamily hd) (qOf F hd) + * (edgeFactor (F.toFamily hd) (qOf F hd) ^ n * G.numEdges) := by + cases n with + | zero => simp + | succ n => + rw [Function.iterate_succ_apply', Dinur.numVerts_step, numEdges_iterStep] + refine le_trans (Nat.mul_le_mul_left _ (Nat.mul_le_mul_right _ ?_)) (Nat.le_add_left _ _) + exact Nat.pow_le_pow_right (one_le_edgeFactor F hd) (Nat.le_succ n) + +theorem size_mono {V V' E E' C : ℕ} (h : V' ≤ V) (hE : E' ≤ E) : + 2 * V' + 4 + E' * (8 * V' + 4 * C + 10) ≤ 2 * V + 4 + E * (8 * V + 4 * C + 10) := by + have h1 : 8 * V' + 4 * C + 10 ≤ 8 * V + 4 * C + 10 := by omega + have h2 := Nat.mul_le_mul hE h1 + omega + +/-- **The size of what a round writes.** -/ +theorem length_encGraph_iterStep_le (G : ConstraintGraph DinurAlpha) (n : ℕ) : + (encGraph ((Dinur.step (F.toFamily hd) (qOf F hd) (qOf_pos F hd))^[n] G)).length + ≤ 2 * (G.numVerts + vertFactor (F.toFamily hd) (qOf F hd) + * (edgeFactor (F.toFamily hd) (qOf F hd) ^ n * G.numEdges)) + 4 + + edgeFactor (F.toFamily hd) (qOf F hd) ^ n * G.numEdges + * (8 * (G.numVerts + vertFactor (F.toFamily hd) (qOf F hd) + * (edgeFactor (F.toFamily hd) (qOf F hd) ^ n * G.numEdges)) + + 4 * Fintype.card (DinurAlpha → DinurAlpha → Bool) + 10) := by + have hV := numVerts_iterStep_le F hd G n + have hE := numEdges_iterStep F hd G n + refine le_trans (length_encGraph_le _) ?_ + rw [hE] + exact size_mono hV (le_refl _) + +/-- **The size of what a round writes**, in terms of a bound on the growth +factor, so that one bound serves every round of the iteration. -/ +theorem length_encGraph_iterStep_le' (G : ConstraintGraph DinurAlpha) (n r B : ℕ) + (hn : n ≤ r) (hB : edgeFactor (F.toFamily hd) (qOf F hd) ^ r ≤ B) : + (encGraph ((Dinur.step (F.toFamily hd) (qOf F hd) (qOf_pos F hd))^[n] G)).length + ≤ 2 * (G.numVerts + vertFactor (F.toFamily hd) (qOf F hd) * (B * G.numEdges)) + 4 + + B * G.numEdges + * (8 * (G.numVerts + vertFactor (F.toFamily hd) (qOf F hd) * (B * G.numEdges)) + + 4 * Fintype.card (DinurAlpha → DinurAlpha → Bool) + 10) := by + have hpow : edgeFactor (F.toFamily hd) (qOf F hd) ^ n ≤ B := + le_trans (Nat.pow_le_pow_right (one_le_edgeFactor F hd) hn) hB + have hV : ((Dinur.step (F.toFamily hd) (qOf F hd) (qOf_pos F hd))^[n] G).numVerts + ≤ G.numVerts + vertFactor (F.toFamily hd) (qOf F hd) * (B * G.numEdges) := + le_trans (numVerts_iterStep_le F hd G n) + (Nat.add_le_add_left (Nat.mul_le_mul_left _ (Nat.mul_le_mul_right _ hpow)) _) + have hE : ((Dinur.step (F.toFamily hd) (qOf F hd) (qOf_pos F hd))^[n] G).numEdges + ≤ B * G.numEdges := by + rw [numEdges_iterStep] + exact Nat.mul_le_mul_right _ hpow + exact le_trans (length_encGraph_le _) (size_mono hV hE) + +/-- The amplifier's round is the round the algorithm computes. -/ +theorem transform_amplifier : + (Dinur.amplifier (F.toFamily hd)).transform + = Dinur.step (F.toFamily hd) (qOf F hd) (qOf_pos F hd) := by + dsimp only [Dinur.amplifier, qOf] + +/-- **So the amplifier's iterate is the algorithm's.** -/ +theorem iter_amplifier (G : ConstraintGraph DinurAlpha) (k : ℕ) : + (Dinur.amplifier (F.toFamily hd)).iter k G + = (Dinur.step (F.toFamily hd) (qOf F hd) (qOf_pos F hd))^[k] G := by + rw [Amplifier.iter, transform_amplifier] + +/-- **The size of what a round writes**, from numeric bounds alone: a caller +supplies bounds on the graph it starts from and on the growth factor. -/ +theorem length_encGraph_iterStep_le'' (G : ConstraintGraph DinurAlpha) (n r B V m : ℕ) + (hn : n ≤ r) (hB : edgeFactor (F.toFamily hd) (qOf F hd) ^ r ≤ B) + (hV : G.numVerts ≤ V) (hm : G.numEdges ≤ m) : + (encGraph ((Dinur.step (F.toFamily hd) (qOf F hd) (qOf_pos F hd))^[n] G)).length + ≤ 2 * (V + vertFactor (F.toFamily hd) (qOf F hd) * (B * m)) + 4 + + B * m + * (8 * (V + vertFactor (F.toFamily hd) (qOf F hd) * (B * m)) + + 4 * Fintype.card (DinurAlpha → DinurAlpha → Bool) + 10) := by + refine le_trans (length_encGraph_iterStep_le' F hd G n r B hn hB) (size_mono ?_ ?_) + · exact Nat.add_le_add hV (Nat.mul_le_mul_left _ (Nat.mul_le_mul_left _ hm)) + · exact Nat.mul_le_mul_left _ hm + +/-! ### The iteration -/ + +/-- **The amplified graph, as a string**: as many rounds as the ruler is long. -/ +noncomputable def gapFn (init ruler : List Bool → List Bool) (z : List Bool) : List Bool := + (roundFn F hd)^[(ruler z).length] (init z) + +/-- **What it writes**: the amplifier's iterate on whatever graph the start +writes. -/ +theorem gapFn_eq {init ruler : List Bool → List Bool} {z : List Bool} + {G : ConstraintGraph DinurAlpha} (h : init z = encGraph G) : + gapFn F hd init ruler z + = encGraph ((Dinur.step (F.toFamily hd) (qOf F hd) (qOf_pos F hd))^[(ruler z).length] G) := by + rw [gapFn, h] + exact iterate_encGraph (roundFn_eq F hd) _ G + +/-- **And writing it is polynomial-time.** -/ +theorem gapFn_mem_FP {init ruler width : List Bool → List Bool} + (hinit : init ∈ FP) (hruler : ruler ∈ FP) (hwidth : width ∈ FP) + (hinitG : ∀ z, ∃ G : ConstraintGraph DinurAlpha, init z = encGraph G) + (hbound : ∀ (z : List Bool) (G : ConstraintGraph DinurAlpha), init z = encGraph G → + ∀ n ≤ (ruler z).length, + (encGraph ((Dinur.step (F.toFamily hd) (qOf F hd) (qOf_pos F hd))^[n] G)).length + ≤ (width z).length) : + gapFn F hd init ruler ∈ FP := + iterate_mem_FP_encGraph (roundFn_mem_FP F hd) hinit hruler hwidth + (roundFn_eq F hd) hinitG hbound + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgGapAll.lean b/Complexitylib/Classes/PCP/Internal/AlgGapAll.lean new file mode 100644 index 00000000..c749768c --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgGapAll.lean @@ -0,0 +1,179 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.PCP.Internal.AlgGap +public import Complexitylib.Classes.PCP.Internal.AlgInit +public import Complexitylib.Classes.PCP.Internal.AlgFormula +public import Complexitylib.Classes.PCP.Internal.AlgUniform + +/-! +# The gap graph of an input + +Everything the reduction does, in one function: read the formula, write its +constraint graph padded to a size the input's length decides, and amplify it as +many times as a logarithmic ruler is long. + +## Main definitions + +- `Complexity.gapAllG` — the graph itself +- `Complexity.gapAll` — that graph, as a string + +## Main results + +- `Complexity.gapAll_eq` — the string is the graph's encoding +- `Complexity.satisfiable_gapAllG` — completeness +- `Complexity.gap_le_unsatVal_gapAllG` — soundness +-/ + +@[expose] public section + +set_option maxRecDepth 8000 + +namespace Complexity + +open Dinur SAT + +variable (F : FinBase) (hd : 1 < F.deg) (E padU : List Bool → List Bool) + {Φ : List Bool → CNF} + +/-- How many rounds an input gets: enough that the padded edge count is below +`2 ^ rounds`. -/ +noncomputable def gapRuler (x : List Bool) : List Bool := logRuler (padU x) + +theorem gapRuler_mem_FP (hpad : padU ∈ FP) : gapRuler padU ∈ FP := + mem_FP_of_eq (mem_FP_comp hpad logRuler_mem_FP) fun _ => rfl + +@[simp] theorem length_gapRuler (x : List Bool) : + (gapRuler padU x).length = rulerLen (padU x).length := by + rw [gapRuler, length_logRuler] + +/-- **The gap graph of an input.** -/ +noncomputable def gapAllG (x : List Bool) : ConstraintGraph DinurAlpha := + (Dinur.step (F.toFamily hd) (qOf F hd) (qOf_pos F hd))^[rulerLen (padU x).length] + ((baseCSP (Φ x)).padGraph (numVerts_baseCSP_pos (Φ x)) (padU x).length) + +/-- **The gap graph of an input, as a string.** -/ +noncomputable def gapAll : List Bool → List Bool := + gapFn F hd (basePadFn E padU baseCodeFn) (gapRuler padU) + +theorem gapAll_eq (hE : ∀ x, E x = (Φ x).encode) (h3 : ∀ x, CNF.Is3CNF (Φ x)) + (hmark : ∀ x, padU x = List.replicate (padU x).length true) + (hle : ∀ x, 3 * (Φ x).length ≤ (padU x).length) (x : List Bool) : + gapAll F hd E padU x = encGraph (gapAllG F hd padU (Φ := Φ) x) := by + rw [gapAll, gapFn_eq F hd (basePadFn_eq E hE h3 x (hmark x) (hle x)), gapAllG, + length_gapRuler] + +/-! ### The gap -/ + +theorem numEdges_padded_le (x : List Bool) (hle : 3 * (Φ x).length ≤ (padU x).length) : + ((baseCSP (Φ x)).padGraph (numVerts_baseCSP_pos (Φ x)) (padU x).length).numEdges + ≤ 2 ^ rulerLen (padU x).length := by + rw [ConstraintGraph.numEdges_padGraph, numEdges_baseCSP, max_eq_left (by omega)] + exact le_of_lt (lt_two_pow_rulerLen _) + +/-- **Completeness.** -/ +theorem satisfiable_gapAllG (h3 : ∀ x, CNF.Is3CNF (Φ x)) + (hle : ∀ x, 3 * (Φ x).length ≤ (padU x).length) (x : List Bool) + (h : (Φ x).Satisfiable) : (gapAllG F hd padU (Φ := Φ) x).Satisfiable := by + have hd' := (Amplifier.dichotomy (Dinur.amplifier (F.toFamily hd)) _ + (numEdges_padded_le padU x (hle x))).1 + rw [iter_amplifier] at hd' + exact hd' (ConstraintGraph.satisfiable_padGraph_iff.mpr ((satisfiable_baseCSP_iff (h3 x)).mpr h)) + +/-- **Soundness.** -/ +theorem gap_le_unsatVal_gapAllG (h3 : ∀ x, CNF.Is3CNF (Φ x)) + (hle : ∀ x, 3 * (Φ x).length ≤ (padU x).length) (x : List Bool) + (h : ¬ (Φ x).Satisfiable) : + (Dinur.amplifier (F.toFamily hd)).gap ≤ (gapAllG F hd padU (Φ := Φ) x).unsatVal := by + have hd' := (Amplifier.dichotomy (Dinur.amplifier (F.toFamily hd)) _ + (numEdges_padded_le padU x (hle x))).2 + rw [iter_amplifier] at hd' + exact hd' fun hs => h ((satisfiable_baseCSP_iff (h3 x)).mp + (ConstraintGraph.satisfiable_padGraph_iff.mp hs)) + +/-! ### Writing it is polynomial-time -/ + +/-- The exponent by which the whole iteration can grow the graph. -/ +noncomputable def growthExp : ℕ := rulerLen (edgeFactor (F.toFamily hd) (qOf F hd)) + +theorem pow_edgeFactor_le (m : ℕ) : + edgeFactor (F.toFamily hd) (qOf F hd) ^ rulerLen m ≤ (2 * m + 1) ^ growthExp F hd := by + calc edgeFactor (F.toFamily hd) (qOf F hd) ^ rulerLen m + ≤ (2 ^ growthExp F hd) ^ rulerLen m := + Nat.pow_le_pow_left (le_of_lt (lt_two_pow_rulerLen _)) _ + _ = (2 ^ rulerLen m) ^ growthExp F hd := by + rw [← pow_mul, ← pow_mul, Nat.mul_comm] + _ ≤ (2 * m + 1) ^ growthExp F hd := + Nat.pow_le_pow_left (two_pow_rulerLen_le m) _ + + +/-- How wide the iteration ever gets, as a function of the input's length. The +round's constants are parameters, so that no tactic here ever meets them. -/ +def widthFn (p₀ q : Polynomial ℕ) (v cw d : ℕ) (n : ℕ) : ℕ := + 2 * (2 * p₀.eval n + 1 + v * ((2 * q.eval n + 1) ^ d * q.eval n)) + 4 + + (2 * q.eval n + 1) ^ d * q.eval n + * (8 * (2 * p₀.eval n + 1 + v * ((2 * q.eval n + 1) ^ d * q.eval n)) + cw) + +theorem widthFn_hasRuler (p₀ q : Polynomial ℕ) (v cw d : ℕ) : + HasRuler (widthFn p₀ q v cw d) := by + have hm : HasRuler fun n => q.eval n := HasRuler.of_poly q + have hV : HasRuler fun n => 2 * p₀.eval n + 1 := + HasRuler.add (HasRuler.mul (HasRuler.const 2) (HasRuler.of_poly p₀)) (HasRuler.const 1) + have hB : HasRuler fun n => (2 * q.eval n + 1) ^ d := + HasRuler.pow (HasRuler.add (HasRuler.mul (HasRuler.const 2) hm) (HasRuler.const 1)) _ + have hBm := HasRuler.mul hB hm + have hA := HasRuler.add hV (HasRuler.mul (HasRuler.const v) hBm) + exact HasRuler.add (HasRuler.add (HasRuler.mul (HasRuler.const 2) hA) (HasRuler.const 4)) + (HasRuler.mul hBm (HasRuler.add (HasRuler.mul (HasRuler.const 8) hA) (HasRuler.const cw))) + +set_option maxRecDepth 100000 in +/-- **Writing the gap graph is polynomial-time.** -/ +theorem gapAll_mem_FP (hEfp : E ∈ FP) (hpad : padU ∈ FP) + (hE : ∀ x, E x = (Φ x).encode) (h3 : ∀ x, CNF.Is3CNF (Φ x)) + (hmark : ∀ x, padU x = List.replicate (padU x).length true) + (hle : ∀ x, 3 * (Φ x).length ≤ (padU x).length) (p₀ q : Polynomial ℕ) + (hp₀ : ∀ x, (E x).length ≤ p₀.eval x.length) + (hq : ∀ x, (padU x).length = q.eval x.length) : + gapAll F hd E padU ∈ FP := by + obtain ⟨R, hR, hRlen⟩ := widthFn_hasRuler p₀ q (vertFactor (F.toFamily hd) (qOf F hd)) + (4 * Fintype.card (DinurAlpha → DinurAlpha → Bool) + 10) (growthExp F hd) + refine gapFn_mem_FP F hd (basePadFn_mem_FP E hEfp hpad _) (gapRuler_mem_FP padU hpad) hR + (fun z => ⟨_, basePadFn_eq E hE h3 z (hmark z) (hle z)⟩) ?_ + intro z G hG n hn + have hsame : encGraph G + = encGraph ((baseCSP (Φ z)).padGraph (numVerts_baseCSP_pos (Φ z)) (padU z).length) := by + rw [← hG, basePadFn_eq E hE h3 z (hmark z) (hle z)] + have hV : G.numVerts + = ((baseCSP (Φ z)).padGraph (numVerts_baseCSP_pos (Φ z)) (padU z).length).numVerts := by + rw [← gVerts_encGraph (G := G), hsame, gVerts_encGraph] + have hEd : G.numEdges + = ((baseCSP (Φ z)).padGraph (numVerts_baseCSP_pos (Φ z)) (padU z).length).numEdges := by + rw [← gEdges_encGraph (G := G), hsame, gEdges_encGraph] + have hmq : G.numEdges = q.eval z.length := by + rw [hEd, ConstraintGraph.numEdges_padGraph, numEdges_baseCSP, + max_eq_left (by have := hle z; omega), hq] + have hVle : G.numVerts ≤ 2 * p₀.eval z.length + 1 := by + have h1 := CNF.maxVar_le_encode_length (Φ z) + have h2 := length_le_length_encode (Φ z) + have h3' := hp₀ z + rw [hE z] at h3' + have hvv : G.numVerts = (Φ z).maxVar + 1 + (Φ z).length := by + rw [hV] + rfl + omega + have hn' : n ≤ rulerLen (padU z).length := by + rwa [length_gapRuler] at hn + have hB : edgeFactor (F.toFamily hd) (qOf F hd) ^ rulerLen (padU z).length + ≤ (2 * q.eval z.length + 1) ^ growthExp F hd := by + rw [← hq] + exact pow_edgeFactor_le F hd _ + refine le_trans (length_encGraph_iterStep_le'' F hd G n _ _ _ _ hn' hB hVle + (le_of_eq hmq)) (le_trans (le_of_eq ?_) (hRlen z)) + rw [widthFn] + ring + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgGapCSP.lean b/Complexitylib/Classes/PCP/Internal/AlgGapCSP.lean new file mode 100644 index 00000000..c8e7f595 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgGapCSP.lean @@ -0,0 +1,197 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.PCP.Internal.AlgGapAll +public import Complexitylib.Classes.PCP.Internal.BaseAlg + +/-! +# The gap graph, as an algorithm + +The verifier does not read a graph; it reads *an algorithm* that answers three +questions about one — how many edges, where an edge's ends are, and whether a +pair of symbols satisfies it. This module packages the gap graph that way, by +reading the string the reduction writes. + +The constraint is a bounded-key decision: an edge's record names its constraint +by a code, and the code, together with the two symbol blocks, is all the +constraint depends on. Both are clamped to constant width, so the key is +constant-size even on strings that are not graphs at all. + +## Main definitions + +- `Complexity.gapAlg` — the gap graph as an `AlgCSP` + +## Main results + +- `Complexity.gapAlg_models` — it agrees with the gap graph +-/ + +@[expose] public section + +set_option maxRecDepth 8000 + +namespace Complexity + +open SAT + +variable (F : FinBase) (hd : 1 < F.deg) (E padU : List Bool → List Bool) + +/-! ### Reading the graph -/ + +/-- The graph string a verifier argument names. -/ +noncomputable def gapStr (z : List Bool) : List Bool := + gapAll F hd E padU (pairFst (pairFst z)) + +theorem gapStr_mem_FP (hgap : gapAll F hd E padU ∈ FP) : gapStr F hd E padU ∈ FP := + mem_FP_of_eq (mem_FP_comp (mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP) hgap) + fun _ => rfl + +/-- The code of the constraint the argument names, clamped to the number of +constraints there are. -/ +noncomputable def gapCodeBlk (z : List Bool) : List Bool := + (recThd (pairSnd (gapStr F hd E padU z)) + (pairSnd (pairFst z)).length).take cRel + +theorem gapCodeBlk_mem_FP (hgap : gapAll F hd E padU ∈ FP) : + gapCodeBlk F hd E padU ∈ FP := by + have hidx : (fun z : List Bool => pairSnd (pairFst z)) ∈ FP := + mem_FP_of_eq (mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP) fun _ => rfl + have hrec := gCodeFn_mem_FP hidx (gapStr_mem_FP F hd E padU hgap) + refine mem_FP_of_eq (Cobham.takeLenFn_mem_FP + (constFn_mem_FP (List.replicate cRel false)) hrec) fun z => ?_ + rw [gapCodeBlk, List.length_replicate] + +/-- Everything the constraint depends on: the code and the two symbol blocks. -/ +noncomputable def gapOkKey (z : List Bool) : List Bool := + pair (gapCodeBlk F hd E padU z) ((pairSnd z).take 46) + +theorem gapOkKey_mem_FP (hgap : gapAll F hd E padU ∈ FP) : gapOkKey F hd E padU ∈ FP := + Cobham.pairFn_mem_FP (gapCodeBlk_mem_FP F hd E padU hgap) + (mem_FP_of_eq (Cobham.takeLenFn_mem_FP + (constFn_mem_FP (List.replicate 46 false)) Cobham.sndBlock_mem_FP) + fun z => by rw [List.length_replicate]) + +theorem gapOkKey_length_le (z : List Bool) : + (gapOkKey F hd E padU z).length ≤ 2 * cRel + 48 := by + have h1 : (gapCodeBlk F hd E padU z).length ≤ cRel := by + rw [gapCodeBlk, List.length_take] + omega + have h2 : ((pairSnd z).take 46).length ≤ 46 := by + rw [List.length_take] + omega + rw [gapOkKey, pair_length] + omega + +/-- What the constraint says, of the key alone. -/ +def gapOkPred (k : List Bool) : Prop := + relOfCode DinurAlpha (pairFst k).length + (symDec DinurAlpha ((pairSnd k).take 23)) + (symDec DinurAlpha ((pairSnd k).drop 23)) = true + +/-- The constraint, as a language on the verifier's verdict argument. -/ +noncomputable def gapOk : Language := + {z : List Bool | gapOkPred (gapOkKey F hd E padU z)} + +theorem gapOk_mem_P (hgap : gapAll F hd E padU ∈ FP) : gapOk F hd E padU ∈ P := + mem_P_of_bounded_key (gapOkKey_mem_FP F hd E padU hgap) + (gapOkKey_length_le F hd E padU) gapOkPred + +/-! ### The record -/ + +/-- **The gap graph as an algorithm.** -/ +noncomputable def gapAlg (hgap : gapAll F hd E padU ∈ FP) : AlgCSP where + numEdges x := gEdges (gapAll F hd E padU x) + numEdges_mem := by + refine mem_FP_of_eq (marks_mem_FP (gEdgesFn_mem_FP hgap)) fun x => ?_ + rw [marks_eq, length_posCount_sndBlock] + width := 23 + width_pos := by omega + vert b x e := + cond b (gHead (gapAll F hd E padU x) e) (gTail (gapAll F hd E padU x) e) + vert_mem := by + intro b + have hg : (fun w : List Bool => gapAll F hd E padU (pairFst w)) ∈ FP := + mem_FP_of_eq (mem_FP_comp Cobham.fstBlock_mem_FP hgap) fun _ => rfl + cases b + · refine mem_FP_of_eq (marks_mem_FP (gTailFn_mem_FP Cobham.sndBlock_mem_FP hg)) + fun w => ?_ + simp only [cond_false] + rw [marks_eq, length_recFst_sndBlock] + · refine mem_FP_of_eq (marks_mem_FP (gHeadFn_mem_FP Cobham.sndBlock_mem_FP hg)) + fun w => ?_ + simp only [cond_true] + rw [marks_eq, length_recSnd_sndBlock] + ok := gapOk F hd E padU + ok_mem := gapOk_mem_P F hd E padU hgap + +@[simp] theorem numEdges_gapAlg (hgap : gapAll F hd E padU ∈ FP) (x : List Bool) : + (gapAlg F hd E padU hgap).numEdges x = gEdges (gapAll F hd E padU x) := rfl + +@[simp] theorem width_gapAlg (hgap : gapAll F hd E padU ∈ FP) : + (gapAlg F hd E padU hgap).width = 23 := rfl + +theorem vert_gapAlg_false (hgap : gapAll F hd E padU ∈ FP) (x : List Bool) (e : ℕ) : + (gapAlg F hd E padU hgap).vert false x e = gTail (gapAll F hd E padU x) e := by + simp only [gapAlg, cond_false] + +theorem vert_gapAlg_true (hgap : gapAll F hd E padU ∈ FP) (x : List Bool) (e : ℕ) : + (gapAlg F hd E padU hgap).vert true x e = gHead (gapAll F hd E padU x) e := by + simp only [gapAlg, cond_true] + +@[simp] theorem ok_gapAlg (hgap : gapAll F hd E padU ∈ FP) : + (gapAlg F hd E padU hgap).ok = gapOk F hd E padU := rfl + +/-! ### It models the gap graph -/ + +variable {Φ : List Bool → CNF} + +theorem gapCodeBlk_length (hE : ∀ x, E x = (Φ x).encode) (h3 : ∀ x, CNF.Is3CNF (Φ x)) + (hmark : ∀ x, padU x = List.replicate (padU x).length true) + (hle : ∀ x, 3 * (Φ x).length ≤ (padU x).length) (x : List Bool) (e : ℕ) + (he : e < (gapAllG F hd padU (Φ := Φ) x).numEdges) (a : List Bool) : + (gapCodeBlk F hd E padU (pair (pair x (List.replicate e true)) a)).length + = codeOfRel ((gapAllG F hd padU (Φ := Φ) x).rel ⟨e, he⟩) := by + have hcode : (recThd (pairSnd (gapAll F hd E padU x)) e).length + = codeOfRel ((gapAllG F hd padU (Φ := Φ) x).rel ⟨e, he⟩) := by + rw [length_recThd_sndBlock, gapAll_eq F hd E padU hE h3 hmark hle x, gCode_encGraph] + have hlt : codeOfRel ((gapAllG F hd padU (Φ := Φ) x).rel ⟨e, he⟩) < cRel := by + rw [cRel_eq] + exact codeOfRel_lt _ + rw [gapCodeBlk, gapStr, pairFst_pair, pairFst_pair, + pairSnd_pair, List.length_replicate, List.length_take, hcode] + omega + +/-- **The algorithm models the gap graph.** -/ +theorem gapAlg_models (hgap : gapAll F hd E padU ∈ FP) + (hE : ∀ x, E x = (Φ x).encode) (h3 : ∀ x, CNF.Is3CNF (Φ x)) + (hmark : ∀ x, padU x = List.replicate (padU x).length true) + (hle : ∀ x, 3 * (Φ x).length ≤ (padU x).length) : + (gapAlg F hd E padU hgap).Models (fun x => gapAllG F hd padU (Φ := Φ) x) + (symEnc DinurAlpha 23) (symDec DinurAlpha) where + numEdges_eq x := by + rw [numEdges_gapAlg, gapAll_eq F hd E padU hE h3 hmark hle x, gEdges_encGraph] + tail_eq x e he := by + rw [vert_gapAlg_false, gapAll_eq F hd E padU hE h3 hmark hle x, gTail_encGraph _ e he] + head_eq x e he := by + rw [vert_gapAlg_true, gapAll_eq F hd E padU hE h3 hmark hle x, gHead_encGraph _ e he] + length_enc := length_symEnc_gapAlpha + dec_enc := symDec_symEnc_gapAlpha + ok_iff x e he u v hu hv := by + rw [width_gapAlg] at hu hv + have ha : (u ++ v).length = 46 := by + rw [List.length_append, hu, hv] + have htake : (u ++ v).take 23 = u := by + rw [← hu, List.take_left] + have hdrop : (u ++ v).drop 23 = v := by + rw [← hu, List.drop_left] + have hcode := gapCodeBlk_length F hd E padU hE h3 hmark hle x e he (u ++ v) + have htake46 : (u ++ v).take 46 = u ++ v := List.take_of_length_le (by omega) + show gapOkPred (gapOkKey F hd E padU (pair (pair x (List.replicate e true)) (u ++ v))) ↔ _ + rw [gapOkPred, gapOkKey, pairFst_pair, pairSnd_pair, hcode, + pairSnd_pair, htake46, htake, hdrop, relOfCode_codeOfRel] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgGraph.lean b/Complexitylib/Classes/PCP/Internal/AlgGraph.lean new file mode 100644 index 00000000..bee04537 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgGraph.lean @@ -0,0 +1,211 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.PCP.Internal.UnaryList +public import Complexitylib.Classes.PCP.Internal.ConstraintGraph +public import Complexitylib.Classes.PCP.Internal.Materialize + +/-! +# A constraint graph as a string + +An algorithm that runs Dinur's amplification has to hold a constraint graph in +its hand. Over a fixed finite alphabet a graph is a small amount of data: how +many vertices, and for each edge its two endpoints and its constraint. The +endpoints are numbers below the vertex count, and the constraint — a predicate +on two symbols — is itself one of finitely many, so it too is a number. + +That is the encoding used here: the vertex count in unary, followed by a list of +records, each three unary numbers. Numbering the constraints rather than +tabulating them keeps every record a triple of numbers, so the whole toolkit of +`UnaryList` applies; and because the number of constraints is a constant, +reading one back is a lookup on a bounded key. + +## Main definitions + +- `Complexity.relOfCode`, `Complexity.codeOfRel` — numbering the constraints +- `Complexity.encGraph` — the graph as a string +- `Complexity.gVerts`, `gEdges`, `gTail`, `gHead`, `gCode` — reading it back + +## Main results + +- `Complexity.buildGraph_eq`, `Complexity.buildGraph_mem_FP` — a rule for each + edge writes the graph, in polynomial time +-/ + +@[expose] public section + +namespace Complexity + +/-! ### Numbering the constraints -/ + +variable {α : Type} [Fintype α] [DecidableEq α] + +/-- The constraint a code stands for; the always-false constraint for a code +that is out of range. -/ +noncomputable def relOfCode (α : Type) [Fintype α] [DecidableEq α] (r : ℕ) : α → α → Bool := + if h : r < Fintype.card (α → α → Bool) then (Fintype.equivFin (α → α → Bool)).symm ⟨r, h⟩ + else fun _ _ => false + +/-- The code of a constraint. -/ +noncomputable def codeOfRel (f : α → α → Bool) : ℕ := (Fintype.equivFin (α → α → Bool) f).val + +theorem codeOfRel_lt (f : α → α → Bool) : + codeOfRel f < Fintype.card (α → α → Bool) := Fin.isLt _ + +@[simp] theorem relOfCode_codeOfRel (f : α → α → Bool) : relOfCode α (codeOfRel f) = f := by + rw [relOfCode, dif_pos (codeOfRel_lt f)] + simp only [codeOfRel, Fin.eta, Equiv.symm_apply_apply] + +/-! ### The encoding -/ + +/-- One record for each edge: its two endpoints and the code of its +constraint. -/ +noncomputable def edgeRecs (G : ConstraintGraph α) : List (List Bool × List Bool × List Bool) := + (List.finRange G.numEdges).map fun e => + (List.replicate (G.tail e).val true, + List.replicate (G.head e).val true, List.replicate (codeOfRel (G.rel e)) true) + +@[simp] theorem length_edgeRecs (G : ConstraintGraph α) : + (edgeRecs G).length = G.numEdges := by + rw [edgeRecs, List.length_map, List.length_finRange] + +/-- **A constraint graph, as a string.** -/ +noncomputable def encGraph (G : ConstraintGraph α) : List Bool := + pair (List.replicate G.numVerts true) (DataEncode.bitstringEncode (edgeRecs G)) + +/-! ### Reading it back -/ + +/-- How many vertices an encoded graph has. -/ +def gVerts (z : List Bool) : ℕ := (pairFst z).length + +/-- How many edges. -/ +noncomputable def gEdges (z : List Bool) : ℕ := (posCount (pairSnd z)).length + +/-- The first endpoint of an edge. -/ +noncomputable def gTail (z : List Bool) (e : ℕ) : ℕ := (recFst (pairSnd z) e).length + +/-- The second endpoint. -/ +noncomputable def gHead (z : List Bool) (e : ℕ) : ℕ := (recSnd (pairSnd z) e).length + +/-- The code of the constraint. -/ +noncomputable def gCode (z : List Bool) (e : ℕ) : ℕ := (recThd (pairSnd z) e).length + +variable (G : ConstraintGraph α) + +@[simp] theorem gVerts_encGraph : gVerts (encGraph G) = G.numVerts := by + rw [gVerts, encGraph, pairFst_pair, List.length_replicate] + +@[simp] theorem gEdges_encGraph : gEdges (encGraph G) = G.numEdges := by + rw [gEdges, encGraph, pairSnd_pair, posCount_eq, List.length_replicate, + length_edgeRecs] + +theorem getElem_edgeRecs (e : ℕ) (he : e < G.numEdges) : + (edgeRecs G)[e]'(by rw [length_edgeRecs]; exact he) + = (List.replicate (G.tail ⟨e, he⟩).val true, + List.replicate (G.head ⟨e, he⟩).val true, + List.replicate (codeOfRel (G.rel ⟨e, he⟩)) true) := by + simp only [edgeRecs, List.getElem_map, List.getElem_finRange] + rfl + +@[simp] theorem gTail_encGraph (e : ℕ) (he : e < G.numEdges) : + gTail (encGraph G) e = (G.tail ⟨e, he⟩).val := by + rw [gTail, encGraph, pairSnd_pair, + recFst_eq (l3 := edgeRecs G) (by rw [length_edgeRecs]; exact he) (getElem_edgeRecs G e he), + List.length_replicate] + +@[simp] theorem gHead_encGraph (e : ℕ) (he : e < G.numEdges) : + gHead (encGraph G) e = (G.head ⟨e, he⟩).val := by + rw [gHead, encGraph, pairSnd_pair, + recSnd_eq (l3 := edgeRecs G) (by rw [length_edgeRecs]; exact he) (getElem_edgeRecs G e he), + List.length_replicate] + +theorem gCode_encGraph (e : ℕ) (he : e < G.numEdges) : + gCode (encGraph G) e = codeOfRel (G.rel ⟨e, he⟩) := by + rw [gCode, encGraph, pairSnd_pair, + recThd_eq (l3 := edgeRecs G) (by rw [length_edgeRecs]; exact he) (getElem_edgeRecs G e he), + List.length_replicate] + +/-- **How long a graph's encoding is**, in terms of its two counts. -/ +theorem length_encGraph_le (G : ConstraintGraph α) : + (encGraph G).length + ≤ 2 * G.numVerts + 4 + + G.numEdges * (8 * G.numVerts + 4 * Fintype.card (α → α → Bool) + 10) := by + have hsum : ((edgeRecs G).map fun a => (DataEncode.bitstringEncode a).length).sum + ≤ G.numEdges * (8 * G.numVerts + 4 * Fintype.card (α → α → Bool) + 10) := by + refine le_trans (List.sum_le_card_nsmul _ + (8 * G.numVerts + 4 * Fintype.card (α → α → Bool) + 10) ?_) ?_ + · intro x hx + obtain ⟨e, he, rfl⟩ := List.mem_map.mp hx + obtain ⟨i, hi, rfl⟩ := List.mem_iff_getElem.mp he + rw [length_edgeRecs] at hi + rw [getElem_edgeRecs G i hi, ← encTriple_eq, length_encTriple] + have h3 := codeOfRel_lt (G.rel ⟨i, hi⟩) + omega + · rw [List.length_map, length_edgeRecs, smul_eq_mul] + rw [encGraph, pair_length, List.length_replicate, length_bitstringEncode_list] + omega + +/-! ### Writing one out -/ + +/-- A graph assembled from a vertex count, an edge count and a rule for each +edge record. -/ +noncomputable def buildGraph (nv cnt E : List Bool → List Bool) (z : List Bool) : List Bool := + pair (nv z) (listEncFn E (pair (cnt z) z)) + +theorem buildGraph_mem_FP {nv cnt E : List Bool → List Bool} (hnv : nv ∈ FP) + (hcnt : cnt ∈ FP) (hE : E ∈ FP) : buildGraph nv cnt E ∈ FP := by + have harg := Cobham.pairFn_mem_FP hcnt id_mem_FP + have hlist := mem_FP_comp harg (materialize_mem_FP hE) + exact mem_FP_of_eq (Cobham.pairFn_mem_FP hnv hlist) fun _ => rfl + +/-- **The rule writes the graph.** -/ +theorem buildGraph_eq {nv cnt E : List Bool → List Bool} {z : List Bool} + {G : ConstraintGraph α} (hnv : nv z = List.replicate G.numVerts true) + (hcnt : cnt z = List.replicate G.numEdges true) + (hE : ∀ (e : ℕ) (he : e < G.numEdges), + E (pair z (List.replicate e true)) + = encTriple (List.replicate (G.tail ⟨e, he⟩).val true) + (List.replicate (G.head ⟨e, he⟩).val true) + (List.replicate (codeOfRel (G.rel ⟨e, he⟩)) true)) : + buildGraph nv cnt E z = encGraph G := by + rw [buildGraph, encGraph, hnv, hcnt, ← length_edgeRecs G] + refine congrArg _ (materialize_eq (edgeRecs G) z fun i hi => ?_) + rw [length_edgeRecs] at hi + rw [hE i hi, encTriple_eq, getElem_edgeRecs G i hi] + +/-! ### Reading one, in polynomial time -/ + +theorem gEdgesFn_mem_FP {g : List Bool → List Bool} (hg : g ∈ FP) : + (fun z => posCount (pairSnd (g z))) ∈ FP := + posCount_mem_FP (mem_FP_comp hg Cobham.sndBlock_mem_FP) + +theorem gTailFn_mem_FP {f g : List Bool → List Bool} (hf : f ∈ FP) (hg : g ∈ FP) : + (fun z => recFst (pairSnd (g z)) (f z).length) ∈ FP := + recFst_mem_FP hf (mem_FP_comp hg Cobham.sndBlock_mem_FP) + +theorem gHeadFn_mem_FP {f g : List Bool → List Bool} (hf : f ∈ FP) (hg : g ∈ FP) : + (fun z => recSnd (pairSnd (g z)) (f z).length) ∈ FP := + recSnd_mem_FP hf (mem_FP_comp hg Cobham.sndBlock_mem_FP) + +theorem gCodeFn_mem_FP {f g : List Bool → List Bool} (hf : f ∈ FP) (hg : g ∈ FP) : + (fun z => recThd (pairSnd (g z)) (f z).length) ∈ FP := + recThd_mem_FP hf (mem_FP_comp hg Cobham.sndBlock_mem_FP) + +theorem length_posCount_sndBlock (z : List Bool) : + (posCount (pairSnd z)).length = gEdges z := rfl + +theorem length_recFst_sndBlock (z : List Bool) (e : ℕ) : + (recFst (pairSnd z) e).length = gTail z e := rfl + +theorem length_recSnd_sndBlock (z : List Bool) (e : ℕ) : + (recSnd (pairSnd z) e).length = gHead z e := rfl + +theorem length_recThd_sndBlock (z : List Bool) (e : ℕ) : + (recThd (pairSnd z) e).length = gCode z e := rfl + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgInit.lean b/Complexitylib/Classes/PCP/Internal/AlgInit.lean new file mode 100644 index 00000000..93941e5a --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgInit.lean @@ -0,0 +1,254 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.PCP.Internal.BaseAlg +public import Complexitylib.Classes.PCP.Internal.AlgGraph +public import Complexitylib.Classes.PCP.Internal.PadGraph + +/-! +# The starting graph, written out + +`BaseAlg` reads the starting graph's numbers straight off a formula, which is +all a verifier needs. Amplification needs more: the graph itself, as a string +the round function can consume. This module writes it, reusing that module's +readers. + +An edge's constraint depends on the formula only through the three signs of its +clause and which of the three positions the edge checks — twelve bits in all. +That is what `baseCodeKey` extracts and `relOfSigns` turns back into a +constraint, so the constraint's code is written by a table lookup. + +## Main definitions + +- `Complexity.relOfSigns` — an edge's constraint, as a function of the signs +- `Complexity.baseCodeKey` — the bounded data an edge's constraint depends on + +## Main results + +- `Complexity.rel_baseCSP` — the constraint depends only on the signs +-/ + +@[expose] public section + +namespace Complexity + +open SAT ThreeSATCSP + +variable (E : List Bool → List Bool) + +/-! ### The constraint, from the signs alone -/ + +open Classical in +/-- The constraint of an edge that checks position `p` of a clause whose three +literals have signs `s`: both endpoints name triples in the image of the +alphabet embedding, the first satisfies the clause, and the two agree on the +checked position. -/ +noncomputable def relOfSigns (s : Fin 3 → Bool) (p : Fin 3) : GapAlpha → GapAlpha → Bool := + fun u v => decide (∃ a₁ a₂ : Fin 3 → Bool, alphaEmb a₁ = u ∧ alphaEmb a₂ = v ∧ + (∃ q : Fin 3, a₁ q = s q) ∧ a₁ p = a₂ 0) + +/-- **The constraint depends only on the signs.** -/ +theorem rel_baseCSP (φ : CNF) (e : ℕ) (he : e < (baseCSP φ).numEdges) : + (baseCSP φ).rel ⟨e, he⟩ + = relOfSigns (fun q => (litOf φ (e / 3) q).sign) ⟨e % 3, Nat.mod_lt _ (by omega)⟩ := by + funext u v + show (ConstraintGraph.lift (toGraph φ) alphaEmb).rel ⟨e, he⟩ u v = _ + rw [ConstraintGraph.rel_lift, relOfSigns] + refine decide_eq_decide.mpr ⟨?_, ?_⟩ + · rintro ⟨a₁, a₂, h1, h2, hr⟩ + replace hr : (clauseSat φ (edgeClause e) a₁ && (a₁ (edgePos e) == a₂ 0)) = true := hr + rw [Bool.and_eq_true, clauseSat_eq_true_iff, beq_iff_eq] at hr + exact ⟨a₁, a₂, h1, h2, hr.1, hr.2⟩ + · rintro ⟨a₁, a₂, h1, h2, hq, hagree⟩ + refine ⟨a₁, a₂, h1, h2, ?_⟩ + show (clauseSat φ (edgeClause e) a₁ && (a₁ (edgePos e) == a₂ 0)) = true + rw [Bool.and_eq_true, clauseSat_eq_true_iff, beq_iff_eq] + exact ⟨hq, hagree⟩ + +/-! ### The key -/ + +/-- The data an edge's constraint depends on: the clause's three signs and +which of them the edge reads. -/ +noncomputable def baseCodeKey (w : List Bool) : List Bool := + pair (baseSigns E (pair w [])) + (modFn [false, false, false] (pairSnd w)) + +theorem baseCodeKey_mem_FP (hE : E ∈ FP) : baseCodeKey E ∈ FP := by + have harg : (fun w : List Bool => pair w []) ∈ FP := + Cobham.pairFn_mem_FP id_mem_FP (constFn_mem_FP []) + have hsigns : (fun w : List Bool => baseSigns E (pair w [])) ∈ FP := by + refine mem_FP_of_eq (mem_FP_comp harg (baseSigns_mem_FP E hE)) fun w => ?_ + rw [Function.comp_apply] + have hmod : (fun w : List Bool => modFn [false, false, false] (pairSnd w)) ∈ FP := by + refine mem_FP_of_eq (mem_FP_comp Cobham.sndBlock_mem_FP + (modFn_mem_FP [false, false, false])) fun w => ?_ + rw [Function.comp_apply] + exact Cobham.pairFn_mem_FP hsigns hmod + +theorem baseCodeKey_length_le (w : List Bool) : (baseCodeKey E w).length ≤ 12 := by + have hs : (baseSigns E (pair w [])).length ≤ 3 := length_baseSigns_le E _ + have hm : (modFn [false, false, false] (pairSnd w)).length ≤ 2 := by + rw [modFn_eq (by simp), List.length_replicate] + have : (pairSnd w).length % [false, false, false].length < 3 := by + simpa using Nat.mod_lt _ (by omega) + omega + rw [baseCodeKey, pair_length] + omega + +variable {Φ : List Bool → CNF} + +theorem baseCodeKey_pair (hE : ∀ x, E x = (Φ x).encode) (h3 : ∀ x, CNF.Is3CNF (Φ x)) + (x : List Bool) {e : ℕ} (he : e < 3 * (Φ x).length) : + baseCodeKey E (pair x (List.replicate e true)) + = pair [(litOf (Φ x) (e / 3) 0).sign, (litOf (Φ x) (e / 3) 1).sign, + (litOf (Φ x) (e / 3) 2).sign] (List.replicate (e % 3) true) := by + rw [baseCodeKey, baseSigns_pair E hE h3 x he [], pairSnd_pair, + modFn_eq (by simp) (List.replicate e true), List.length_replicate, + show ([false, false, false] : List Bool).length = 3 from rfl] + +/-! ### The constraint's code, from the key -/ + +/-- The constraint an edge's key stands for. -/ +noncomputable def baseRelOfKey (k : List Bool) : GapAlpha → GapAlpha → Bool := + relOfSigns (fun q => (pairFst k).getD q.val false) + ⟨(pairSnd k).length % 3, Nat.mod_lt _ (by omega)⟩ + +/-- The constraint's code, in unary, from the key. -/ +noncomputable def baseCodeFn (k : List Bool) : List Bool := + List.replicate (codeOfRel (baseRelOfKey k)) true + +theorem relOfSigns_congr {s t : Fin 3 → Bool} {m n : ℕ} (hm : m < 3) (hn : n < 3) + (hs : s = t) (h : m = n) : relOfSigns s ⟨m, hm⟩ = relOfSigns t ⟨n, hn⟩ := by + subst hs + subst h + rfl + +theorem baseCodeFn_codeKey (hE : ∀ x, E x = (Φ x).encode) (h3 : ∀ x, CNF.Is3CNF (Φ x)) + (x : List Bool) (e : ℕ) (he : e < (baseCSP (Φ x)).numEdges) : + baseCodeFn (baseCodeKey E (pair x (List.replicate e true))) + = List.replicate (codeOfRel ((baseCSP (Φ x)).rel ⟨e, he⟩)) true := by + have he' : e < 3 * (Φ x).length := by rwa [numEdges_baseCSP] at he + have hrel : baseRelOfKey (baseCodeKey E (pair x (List.replicate e true))) + = relOfSigns (fun q => (litOf (Φ x) (e / 3) q).sign) + ⟨e % 3, Nat.mod_lt _ (by omega)⟩ := by + rw [baseRelOfKey] + refine relOfSigns_congr _ _ ?_ ?_ + · rw [baseCodeKey_pair E hE h3 x he', pairFst_pair] + funext q + fin_cases q <;> rfl + · rw [baseCodeKey_pair E hE h3 x he', pairSnd_pair, List.length_replicate] + omega + rw [baseCodeFn, hrel, rel_baseCSP] + +/-! ### The counts -/ + +/-- The number of vertices, in unary: one per variable, one per clause. -/ +noncomputable def baseVertsU (z : List Bool) : List Bool := + marks (baseMaxU E z) ++ [true] ++ divC 3 (baseEdgesU E z) + +theorem baseVertsU_mem_FP (hE : E ∈ FP) : baseVertsU E ∈ FP := + Cobham.appendFn_mem_FP + (Cobham.appendFn_mem_FP (marks_mem_FP (baseMaxU_mem_FP E hE)) (constFn_mem_FP [true])) + (divC_mem_FP (baseEdgesU_mem_FP E hE) 3) + +theorem baseVertsU_eq (hE : ∀ x, E x = (Φ x).encode) (h3 : ∀ x, CNF.Is3CNF (Φ x)) + (x : List Bool) : + baseVertsU E x = List.replicate (baseCSP (Φ x)).numVerts true := by + have hnv : (baseCSP (Φ x)).numVerts = ((Φ x).maxVar + 1) + (Φ x).length := rfl + rw [baseVertsU, marks_eq, baseMaxU_eq E hE h3, baseEdgesU_eq E hE, divC_eq (by omega), + List.length_replicate, Nat.mul_div_cancel_left _ (by omega), hnv] + rw [show ([true] : List Bool) = List.replicate 1 true from rfl, + ← List.replicate_add, ← List.replicate_add] + +/-! ### The endpoints -/ + +theorem baseTailU_val (hE : ∀ x, E x = (Φ x).encode) (h3 : ∀ x, CNF.Is3CNF (Φ x)) + (x : List Bool) (e : ℕ) (he : e < (baseCSP (Φ x)).numEdges) : + (baseTailU E (pair x (List.replicate e true))).length + = ((baseCSP (Φ x)).tail ⟨e, he⟩).val := by + rw [numEdges_baseCSP] at he + rw [baseTailU_eq E hE h3] + show _ = (clauseVertex (Φ x) (edgeClause e)).val + rw [clauseVertex, edgeClause, dif_pos (by rw [numVerts]; omega)] + +theorem baseHeadU_val (hE : ∀ x, E x = (Φ x).encode) (h3 : ∀ x, CNF.Is3CNF (Φ x)) + (x : List Bool) (e : ℕ) (he : e < (baseCSP (Φ x)).numEdges) : + (baseHeadU E (pair x (List.replicate e true))).length + = ((baseCSP (Φ x)).head ⟨e, he⟩).val := by + rw [numEdges_baseCSP] at he + have hj : e / 3 < (Φ x).length := by omega + have hp : e % 3 < ((Φ x)[e / 3]'hj).length := by + rw [h3 x _ (List.getElem_mem hj)] + omega + have hvar : (litOf (Φ x) (e / 3) ⟨e % 3, Nat.mod_lt _ (by omega)⟩).var + ≤ (Φ x).maxVar := by + rw [litOf_eq (Φ x) hj _ hp] + exact var_le_maxVar (Φ x) hj hp + rw [baseHeadU_eq E hE h3 x he] + show _ = (varVertex (Φ x) (litOf (Φ x) (edgeClause e) (edgePos e)).var).val + rw [varVertex, edgeClause, edgePos, dif_pos (by rw [numVerts]; omega)] + +/-! ### Padded to a fixed size -/ + +/-- The base graph always has a vertex. -/ +theorem numVerts_baseCSP_pos (φ : CNF) : 0 < (baseCSP φ).numVerts := by + show 0 < ((φ.maxVar + 1) + φ.length) + omega + +/-- The code of the constraint that is always true, in unary. -/ +noncomputable def trivCode : List Bool := + List.replicate (codeOfRel (α := GapAlpha) (fun _ _ => true)) true + +/-- **The starting graph, padded**: the edge count is whatever `padU` says, so it +can be made to depend on the input's length alone. -/ +noncomputable def basePadFn (padU g : List Bool → List Bool) : List Bool → List Bool := + buildGraph (baseVertsU E) padU + (fun w => ifLtLen (pairSnd w) (baseEdgesU E (pairFst w)) + (encTriple (marks (baseTailU E w)) (marks (baseHeadU E w)) (g (baseCodeKey E w))) + (encTriple [] [] trivCode)) + +theorem basePadFn_mem_FP (hE : E ∈ FP) (hP : padU ∈ FP) (g : List Bool → List Bool) : + basePadFn E padU g ∈ FP := by + refine buildGraph_mem_FP (baseVertsU_mem_FP E hE) hP (ifLtLen_mem_FP Cobham.sndBlock_mem_FP + ?_ (encTriple_mem_FP (marks_mem_FP (baseTailU_mem_FP E hE)) + (marks_mem_FP (baseHeadU_mem_FP E hE)) + (mem_FP_of_bounded_key (baseCodeKey_mem_FP E hE) (baseCodeKey_length_le E) g)) + (constFn_mem_FP _)) + exact mem_FP_of_eq (mem_FP_comp Cobham.fstBlock_mem_FP (baseEdgesU_mem_FP E hE)) + fun w => rfl + +/-- **The rule writes the padded starting graph.** -/ +theorem basePadFn_eq (hE : ∀ x, E x = (Φ x).encode) (h3 : ∀ x, CNF.Is3CNF (Φ x)) + (x : List Bool) (hPmark : padU x = List.replicate (padU x).length true) + (hPle : 3 * (Φ x).length ≤ (padU x).length) : + basePadFn E padU baseCodeFn x + = encGraph ((baseCSP (Φ x)).padGraph (numVerts_baseCSP_pos (Φ x)) (padU x).length) := by + have hmax : max (padU x).length (baseCSP (Φ x)).numEdges = (padU x).length := by + rw [numEdges_baseCSP] + omega + refine buildGraph_eq (baseVertsU_eq E hE h3 x) ?_ fun e he => ?_ + · rw [ConstraintGraph.numEdges_padGraph, hmax] + exact hPmark + · rw [ConstraintGraph.numEdges_padGraph, hmax] at he + have hcnt : (baseEdgesU E (pairFst (pair x (List.replicate e true)))).length + = 3 * (Φ x).length := by + rw [pairFst_pair, baseEdgesU_eq E hE, List.length_replicate] + by_cases hlt : e < (baseCSP (Φ x)).numEdges + · have hlt' : e < 3 * (Φ x).length := by rwa [numEdges_baseCSP] at hlt + rw [ifLtLen_pos (by rw [pairSnd_pair, List.length_replicate, hcnt]; exact hlt')] + rw [ConstraintGraph.tail_padGraph_of_lt _ hlt, ConstraintGraph.head_padGraph_of_lt _ hlt, + ConstraintGraph.rel_padGraph_of_lt _ hlt] + rw [marks_eq, marks_eq, baseTailU_val E hE h3 x e hlt, baseHeadU_val E hE h3 x e hlt, + baseCodeFn_codeKey E hE h3 x e hlt] + · rw [ifLtLen_neg (by rw [pairSnd_pair, List.length_replicate, hcnt, + numEdges_baseCSP] at *; omega)] + rw [ConstraintGraph.tail_padGraph_of_ge _ hlt, ConstraintGraph.head_padGraph_of_ge _ hlt, + ConstraintGraph.rel_padGraph_of_ge _ hlt, trivCode] + rfl + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgIter.lean b/Complexitylib/Classes/PCP/Internal/AlgIter.lean new file mode 100644 index 00000000..2dde58e0 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgIter.lean @@ -0,0 +1,59 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.AlgEdge + +/-! +# Iterating a round + +Amplification runs a round logarithmically many times. On the algorithm's side +that is an iteration of one `FP` function; on the abstract side it is +`Amplifier.iter`. This module says the two agree, for any round function that +computes its transformation. + +## Main results + +- `Complexity.iterate_encGraph` — iterating the algorithm writes the iterated + graph +- `Complexity.iterate_mem_FP_encGraph` — and the iteration is an `FP` function +-/ + +@[expose] public section + +namespace Complexity + +variable {α : Type} [Fintype α] [DecidableEq α] + +/-- **Iterating the algorithm writes the iterated graph.** -/ +theorem iterate_encGraph {f : List Bool → List Bool} + {T : ConstraintGraph α → ConstraintGraph α} + (hstep : ∀ G : ConstraintGraph α, f (encGraph G) = encGraph (T G)) : + ∀ (k : ℕ) (G : ConstraintGraph α), f^[k] (encGraph G) = encGraph (T^[k] G) := by + intro k + induction k with + | zero => intro G; rfl + | succ k ih => + intro G + rw [Function.iterate_succ_apply, Function.iterate_succ_apply, hstep, ih] + +/-- **The iteration is an `FP` function.** The bound the iteration rule wants is +supplied on the *graphs*, where the round's size bounds live, rather than on +their encodings. -/ +theorem iterate_mem_FP_encGraph {f init ruler width : List Bool → List Bool} + {T : ConstraintGraph α → ConstraintGraph α} + (hf : f ∈ FP) (hinit : init ∈ FP) (hruler : ruler ∈ FP) (hwidth : width ∈ FP) + (hstep : ∀ G : ConstraintGraph α, f (encGraph G) = encGraph (T G)) + (hinitG : ∀ z, ∃ G : ConstraintGraph α, init z = encGraph G) + (hbound : ∀ (z : List Bool) (G : ConstraintGraph α), init z = encGraph G → + ∀ n ≤ (ruler z).length, (encGraph (T^[n] G)).length ≤ (width z).length) : + (fun z => f^[(ruler z).length] (init z)) ∈ FP := by + refine Cobham.iterate_mem_FP hf hinit hruler hwidth ?_ + intro z n hn + obtain ⟨G, hG⟩ := hinitG z + rw [hG, iterate_encGraph hstep] + exact hbound z G hG n hn + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgKey.lean b/Complexitylib/Classes/PCP/Internal/AlgKey.lean new file mode 100644 index 00000000..eeb9e81f --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgKey.lean @@ -0,0 +1,244 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.AlgPosNum +public import Complexitylib.Classes.PCP.Internal.AlgStep + +/-! +# The bounded data a composed edge depends on + +A composed edge's second endpoint and its constraint depend on the outer graph +only through what one killed walk meets: the darts it takes, the parity of each +vertex it stands on, the code of each constraint there, and the darts it returns +by — together with the random string and the read. All of that lives in finite +types that do not mention the graph, so it is a *key* of bounded length, and the +edge's data is a function of the key alone. + +## Main definitions + +- `Complexity.StepKey` — that data +- `Complexity.packKey`, `Complexity.keyOfString` — writing it out as a string, + and reading it back +- `Complexity.relOfKey` — the constraint it describes +- `Complexity.satSetOfKey` — and the satisfying set + +## Main results + +- `Complexity.keyOfString_packKey` — the reading inverts the writing +- `Complexity.relOfKey_stepKeyOf` — on a walk's own data it is the killed + constraint +- `Complexity.cubeOfKey_eq`, `Complexity.codeOfKey_eq` — so the data alone gives + the composed edge's second endpoint and its constraint +-/ + +@[expose] public section + +namespace Complexity + +open BooleanAnalysis Tester + +/-- The bounded data a composed edge's head and constraint depend on. -/ +abbrev StepKey (E : ExpanderFamily) (T q B C : ℕ) : Type := + ((Fin T → PreDart E) × (Fin T → Fin q)) + × ((Fin T → Fin 2) × (Fin T → Fin C)) + × ((Fin T → PreDart E) × (Cube (ROf B) × ReadIdx)) + +namespace StepKey + +variable {E : ExpanderFamily} {T q B C : ℕ} (k : StepKey E T q B C) + +/-- The walk's darts. -/ +def dart : Fin T → PreDart E := k.1.1 + +/-- Its coins. -/ +def coins : Fin T → Fin q := k.1.2 + +/-- The parity of the vertex each step stands on. -/ +def par : Fin T → Fin 2 := k.2.1.1 + +/-- The code of the constraint each step meets. -/ +def code : Fin T → Fin C := k.2.1.2 + +/-- The darts the walk returns by. -/ +def rev : Fin T → PreDart E := k.2.2.1 + +/-- The tester's random string. -/ +def rand : Cube (ROf B) := k.2.2.2.1 + +/-- The read. -/ +def read : ReadIdx := k.2.2.2.2 + +/-- How long the effective walk is. -/ +def len : ℕ := stopAt k.coins + +theorem len_le : k.len ≤ T := stopAt_le _ + +end StepKey + +variable {α : Type} [Fintype α] [DecidableEq α] [Nonempty α] + +/-- The constraint the data describes: at each step of the effective walk, the +preprocessed constraint its code and parity stand for. -/ +noncomputable def relOfKey {E : ExpanderFamily} {T q B C : ℕ} (k : StepKey E T q B C) : + (PreWalk E T → α) → (PreWalk E T → α) → Bool := + preRelOfSteps E T E.degree k.len + (fun i => k.dart ⟨i.val, lt_of_lt_of_le i.isLt k.len_le⟩) + (fun i => (k.par ⟨i.val, lt_of_lt_of_le i.isLt k.len_le⟩).val) + (fun i => (k.code ⟨i.val, lt_of_lt_of_le i.isLt k.len_le⟩).val) + (fun i => ⟨⟨i.val, by have := i.isLt; have := k.len_le; omega⟩, + fun j => k.dart ⟨j.val, by have := j.isLt; have := i.isLt; have := k.len_le; omega⟩⟩) + (fun i => ⟨⟨k.len - (i.val + 1), by have := i.isLt; have := k.len_le; omega⟩, + fun j => k.rev ⟨j.val, by have := j.isLt; have := i.isLt; have := k.len_le; omega⟩⟩) + +set_option synthInstance.maxSize 400 in +/-- The satisfying set the data describes. -/ +noncomputable def satSetOfKey {E : ExpanderFamily} {T q B C : ℕ} + (encβ : (PreWalk E T → α) → Cube B) (k : StepKey E T q B C) : + Finset (Cube (kOf B)) := + (Finset.univ.filter fun st : (PreWalk E T → α) × (PreWalk E T → α) => + relOfKey k st.1 st.2 = true).image fun st => RegCSP.inputVec encβ st.1 st.2 + +/-! ### Writing the data out -/ + +/-- The value a number names, or a default. -/ +noncomputable def decOr {X : Type} [NumEnc X] (d : X) (n : ℕ) : X := (NumEnc.dec n).getD d + +theorem decOr_enc {X : Type} [NumEnc X] (d a : X) : decOr d (NumEnc.enc a) = a := by + rw [decOr, NumEnc.dec_enc] + rfl + +variable {E : ExpanderFamily} {T q B C : ℕ} + +/-- The data written out: one unary number per component. -/ +noncomputable def packKey (k : StepKey E T q B C) : List Bool := + pair (pair (List.replicate (NumEnc.enc k.dart) true) + (List.replicate (NumEnc.enc k.coins) true)) + (pair (pair (List.replicate (NumEnc.enc k.par) true) + (List.replicate (NumEnc.enc k.code) true)) + (pair (List.replicate (NumEnc.enc k.rev) true) + (pair (List.replicate (NumEnc.enc k.rand) true) + (List.replicate (NumEnc.enc k.read) true)))) + +/-- The data read back from a string, falling back on a default. -/ +noncomputable def keyOfString (dflt : StepKey E T q B C) (s : List Bool) : + StepKey E T q B C := + ((decOr dflt.dart (pairFst (pairFst s)).length, + decOr dflt.coins (pairSnd (pairFst s)).length), + ((decOr dflt.par + (pairFst (pairFst (pairSnd s))).length, + decOr dflt.code + (pairSnd (pairFst (pairSnd s))).length), + (decOr dflt.rev (pairFst (pairSnd (pairSnd s))).length, + (decOr dflt.rand (pairFst (pairSnd + (pairSnd (pairSnd s)))).length, + decOr dflt.read (pairSnd (pairSnd + (pairSnd (pairSnd s)))).length)))) + +/-- **The reading inverts the writing.** -/ +theorem keyOfString_packKey (dflt k : StepKey E T q B C) : + keyOfString dflt (packKey k) = k := by + rw [keyOfString, packKey] + simp only [pairFst_pair, pairSnd_pair, List.length_replicate, decOr_enc] + rfl + +/-- **A digit sum is a tuple's number**, when the digits are the entries'. -/ +theorem length_digitSum_eq_enc {X : Type} [NumEnc X] {T : ℕ} (s : Fin T → X) + (digit : ℕ → List Bool → List Bool) (w : List Bool) + (h : ∀ (j : ℕ) (hj : j < T), (digit j w).length = NumEnc.enc (s ⟨j, hj⟩)) : + (digitSum (NumEnc.card X) digit T w).length = NumEnc.enc s := by + rw [length_digitSum] + show _ = ∑ j ∈ Finset.range T, NumEnc.encAt s j * NumEnc.card X ^ j + refine Finset.sum_congr rfl fun j hj => ?_ + rw [Finset.mem_range] at hj + rw [h j hj, NumEnc.encAt, dif_pos hj] + +/-- The data a killed walk actually shows. -/ +noncomputable def stepKeyOf (G : ConstraintGraph α) (E : ExpanderFamily) {q T : ℕ} + (v : (G.preprocess E).graph.V) + (x : (Fin T → (G.preprocess E).graph.D) × (Fin T → Fin q)) + (B : ℕ) (z : Cube (ROf B)) (i : ReadIdx) : + StepKey E T q B (Fintype.card (α → α → Bool)) := + ((x.1, x.2), + ((fun j => if h : j.val < (G.preprocess E).graph.kLen x then + ⟨NumEnc.enc ((G.preprocess E).graph.walkAt ((G.preprocess E).graph.kLen x) v + ((G.preprocess E).graph.kWalk x) j.val) % 2, Nat.mod_lt _ (by omega)⟩ + else 0, + fun j => if h : j.val < (G.preprocess E).graph.kLen x then + ⟨codeOfRel (G.rel ((G.preprocess E).graph.walkAt ((G.preprocess E).graph.kLen x) v + ((G.preprocess E).graph.kWalk x) j.val).1), codeOfRel_lt _⟩ + else 0), + ((G.preprocess E).graph.killedRev v x.1 x.2, (z, i)))) + +omit [Nonempty α] in +/-- **The data a walk shows describes that walk's constraint.** -/ +theorem relOfKey_stepKeyOf (G : ConstraintGraph α) (E : ExpanderFamily) {q T : ℕ} + (hq : 0 < q) (v : (G.preprocess E).graph.V) + (x : (Fin T → (G.preprocess E).graph.D) × (Fin T → Fin q)) + (B : ℕ) (z : Cube (ROf B)) (i : ReadIdx) : + relOfKey (α := α) (stepKeyOf G E v x B z i) + = ((G.preprocess E).killedPow q T hq).rel v x := by + rw [rel_killedPow_eq_preRelOfSteps, relOfKey] + simp only [stepKeyOf, StepKey.dart, StepKey.par, StepKey.code, StepKey.rev, StepKey.coins, + StepKey.len, RegGraph.kLen, RegGraph.kWalk, Fin.is_lt, dif_pos] + congr 1 + funext i + congr 1 + funext j + rw [RegGraph.killedRev, RegGraph.extWalk] + dsimp only + rw [dif_pos (by have := j.isLt; omega)] + +set_option synthInstance.maxSize 400 in +omit [Nonempty α] in +/-- **The data a walk shows describes that walk's satisfying set.** -/ +theorem satSetOfKey_stepKeyOf (G : ConstraintGraph α) (E : ExpanderFamily) {q T : ℕ} + (hq : 0 < q) (v : (G.preprocess E).graph.V) + (x : (Fin T → (G.preprocess E).graph.D) × (Fin T → Fin q)) + {B : ℕ} (z : Cube (ROf B)) (i : ReadIdx) (encβ : (PreWalk E T → α) → Cube B) : + satSetOfKey encβ (stepKeyOf G E v x B z i) + = ((G.preprocess E).killedPow q T hq).satSet encβ (v, x) := by + show (Finset.univ.filter fun st : (PreWalk E T → α) × (PreWalk E T → α) => + relOfKey (stepKeyOf G E v x B z i) st.1 st.2 = true).image + (fun st => RegCSP.inputVec encβ st.1 st.2) = _ + rw [relOfKey_stepKeyOf (hq := hq)] + rfl + +/-- The cube the data names. -/ +noncomputable def cubeOfKey {E : ExpanderFamily} {T q B C : ℕ} + (encβ : (PreWalk E T → α) → Cube B) (k : StepKey E T q B C) : ℕ := + RegCSP.cubeOfSet (satSetOfKey encβ k) k.rand k.read + +/-- The constraint code the data names. -/ +noncomputable def codeOfKey {E : ExpanderFamily} {T q B C : ℕ} + (encβ : (PreWalk E T → α) → Cube B) (k : StepKey E T q B C) : ℕ := + codeOfRel (MultiTest.relOfCheck (RegCSP.checkOfSet (satSetOfKey encβ k) k.rand) k.read) + +omit [Nonempty α] in +/-- **The data gives the composed edge's cube.** -/ +theorem cubeOfKey_eq (G : ConstraintGraph α) (E : ExpanderFamily) {q T : ℕ} + (hq : 0 < q) (v : (G.preprocess E).graph.V) + (x : (Fin T → (G.preprocess E).graph.D) × (Fin T → Fin q)) + {B : ℕ} (z : Cube (ROf B)) (i : ReadIdx) (encβ : (PreWalk E T → α) → Cube B) : + cubeOfKey encβ (stepKeyOf G E v x B z i) + = ((G.preprocess E).killedPow q T hq).cubeNum encβ (v, x) z i := by + rw [cubeOfKey, satSetOfKey_stepKeyOf G E hq v x z i encβ, + RegCSP.cubeNum_eq_cubeOfSet] + rfl + +omit [Nonempty α] in +/-- **And the composed edge's constraint.** -/ +theorem codeOfKey_eq (G : ConstraintGraph α) (E : ExpanderFamily) {q T : ℕ} + (hq : 0 < q) (v : (G.preprocess E).graph.V) + (x : (Fin T → (G.preprocess E).graph.D) × (Fin T → Fin q)) + {B : ℕ} (z : Cube (ROf B)) (i : ReadIdx) (encβ : (PreWalk E T → α) → Cube B) : + codeOfKey encβ (stepKeyOf G E v x B z i) + = codeOfRel (MultiTest.relOfCheck + ((((G.preprocess E).killedPow q T hq).compose encβ).check (v, x) z) i) := by + rw [codeOfKey, satSetOfKey_stepKeyOf G E hq v x z i encβ, + RegCSP.check_eq_checkOfSet] + rfl + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgKeyFn.lean b/Complexitylib/Classes/PCP/Internal/AlgKeyFn.lean new file mode 100644 index 00000000..e32a8dff --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgKeyFn.lean @@ -0,0 +1,673 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.PCP.Internal.AlgKey +public import Complexitylib.Classes.PCP.Internal.AlgKilled + +/-! +# Reading an edge's data off the input + +The constants of a round — the expander's degree, the walk's length, the killing +rate, how many constraints the alphabet has, how many random strings the tester +uses — are fixed. Given them, an edge number of the composed graph splits by +division into a test, a random string and a read; the test splits into a vertex +and a killed dart; and the killed dart drives the walk, whose parities, codes +and return darts are what the edge's data is made of. + +This module writes that out as `FP` functions. + +## Main definitions + +- `Complexity.Round` — the constants of a round +- `Complexity.keyFn` — the edge's data, as a string + +## Main results + +- `Complexity.keyFn_mem_FP` — it is an `FP` function +- `Complexity.keyFn_length_le` — of bounded length, whatever the input +- `Complexity.keyFn_eq` — on a real edge it writes that edge's own data +- `Complexity.cubeFn_eq`, `Complexity.codeFn_eq` — so the composed edge's second + endpoint and its constraint are `FP` functions of the input +-/ + +@[expose] public section + +namespace Complexity + +open BooleanAnalysis Tester + +/-- The constants of one round of amplification, as an algorithm sees them. -/ +structure Round where + /-- The expander's degree. -/ + deg : ℕ + /-- The preprocessed system's degree. -/ + P : ℕ + /-- The walk's length. -/ + T : ℕ + /-- The killing rate. -/ + q : ℕ + /-- How many constraints there are on the alphabet. -/ + C : ℕ + /-- How many random strings the tester has. -/ + cZ : ℕ + +namespace Round + +variable (r : Round) + +/-- How many darts the powered graph has at a vertex. -/ +def cD : ℕ := r.P ^ r.T * r.q ^ r.T + +/-- How many coin tuples there are. -/ +def cQ : ℕ := r.q ^ r.T + +end Round + +variable (F : FinBase) (pol : Polynomial ℕ) (r : Round) + +/-! ### Splitting an edge number -/ + +/-- The test an edge belongs to. -/ +noncomputable def testFn (w : List Bool) : List Bool := + divC (r.cZ * 22) (pairSnd w) + +/-- The random string it runs on. -/ +noncomputable def randFn (w : List Bool) : List Bool := + divC 22 (modC (r.cZ * 22) (pairSnd w)) + +/-- The read it asks for. -/ +noncomputable def readFn (w : List Bool) : List Bool := modC 22 (pairSnd w) + +/-- The killed dart the test is. -/ +noncomputable def dartFn (w : List Bool) : List Bool := modC r.cD (testFn r w) + +/-- The vertex it starts at. -/ +noncomputable def vertFn (w : List Bool) : List Bool := divC r.cD (testFn r w) + +/-- The walk's steps. -/ +noncomputable def stepsFn (w : List Bool) : List Bool := divC r.cQ (dartFn r w) + +/-- The walk's coins. -/ +noncomputable def coinFn (w : List Bool) : List Bool := modC r.cQ (dartFn r w) + +/-- The input the walk algorithm reads. -/ +noncomputable def walkArg (w : List Bool) : List Bool := + pair (pairFst w) (pair (vertFn r w) (stepsFn r w)) + +/-- The input the killed-walk algorithms read. -/ +noncomputable def killArg (w : List Bool) : List Bool := + pair (pairFst w) (pair (vertFn r w) (dartFn r w)) + +theorem testFn_mem_FP : testFn r ∈ FP := divC_mem_FP Cobham.sndBlock_mem_FP _ + +theorem randFn_mem_FP : randFn r ∈ FP := + divC_mem_FP (modC_mem_FP Cobham.sndBlock_mem_FP _) _ + +theorem readFn_mem_FP : readFn ∈ FP := modC_mem_FP Cobham.sndBlock_mem_FP _ + +theorem dartFn_mem_FP : dartFn r ∈ FP := modC_mem_FP (testFn_mem_FP r) _ + +theorem vertFn_mem_FP : vertFn r ∈ FP := divC_mem_FP (testFn_mem_FP r) _ + +theorem stepsFn_mem_FP : stepsFn r ∈ FP := divC_mem_FP (dartFn_mem_FP r) _ + +theorem coinFn_mem_FP : coinFn r ∈ FP := modC_mem_FP (dartFn_mem_FP r) _ + +theorem walkArg_mem_FP : walkArg r ∈ FP := + Cobham.pairFn_mem_FP Cobham.fstBlock_mem_FP + (Cobham.pairFn_mem_FP (vertFn_mem_FP r) (stepsFn_mem_FP r)) + +theorem killArg_mem_FP : killArg r ∈ FP := + Cobham.pairFn_mem_FP Cobham.fstBlock_mem_FP + (Cobham.pairFn_mem_FP (vertFn_mem_FP r) (dartFn_mem_FP r)) + +/-! ### The walk's data -/ + +/-- Where the walk stops. -/ +noncomputable def stopBlk (w : List Bool) : List Bool := + stopFn r.q (coinsOf r.q r.T) 0 r.T (killArg r w) + +theorem stopBlk_mem_FP : stopBlk r ∈ FP := + mem_FP_of_eq (mem_FP_comp (killArg_mem_FP r) + (stopFn_mem_FP (coinsOf_mem_FP r.q r.T) r.T 0)) fun _ => rfl + +/-- The parity of the vertex the `i`-th step stands on, and `0` past the end. -/ +noncomputable def parDigit (i : ℕ) (w : List Bool) : List Bool := + ifLtLen (List.replicate i true) (stopBlk r w) + (modC 2 (walkFn F pol r.deg r.P i (walkArg r w))) [] + +theorem parDigit_mem_FP (i : ℕ) : parDigit F pol r i ∈ FP := + ifLtLen_mem_FP (constFn_mem_FP _) (stopBlk_mem_FP r) + (modC_mem_FP (mem_FP_of_eq + (mem_FP_comp (walkArg_mem_FP r) (walkFn_mem_FP F pol r.deg r.P i)) fun _ => rfl) 2) + (constFn_mem_FP []) + +/-- The code of the constraint the `i`-th step meets, and `0` past the end. -/ +noncomputable def codeDigit (i : ℕ) (w : List Bool) : List Bool := + ifLtLen (List.replicate i true) (stopBlk r w) + ((recThd (pairSnd (pairFst w)) + (divC 2 (walkFn F pol r.deg r.P i (walkArg r w))).length).take r.C) [] + +theorem codeDigit_mem_FP (i : ℕ) : codeDigit F pol r i ∈ FP := by + have hwalk : (fun w : List Bool => walkFn F pol r.deg r.P i (walkArg r w)) ∈ FP := + mem_FP_of_eq (mem_FP_comp (walkArg_mem_FP r) (walkFn_mem_FP F pol r.deg r.P i)) + fun _ => rfl + have hcode : (fun w : List Bool => recThd (pairSnd (pairFst w)) + (divC 2 (walkFn F pol r.deg r.P i (walkArg r w))).length) ∈ FP := + gCodeFn_mem_FP (divC_mem_FP hwalk 2) Cobham.fstBlock_mem_FP + refine ifLtLen_mem_FP (constFn_mem_FP _) (stopBlk_mem_FP r) ?_ (constFn_mem_FP []) + have := Cobham.takeLenFn_mem_FP (constFn_mem_FP (List.replicate r.C true)) hcode + refine mem_FP_of_eq this fun w => ?_ + rw [List.length_replicate] + +/-! ### The data as a string -/ + +/-- **An edge's data**, written out as the seven blocks `packKey` expects. -/ +noncomputable def keyFn (w : List Bool) : List Bool := + pair (pair (stepsFn r w) (coinFn r w)) + (pair (pair (marks (digitSum 2 (parDigit F pol r) r.T w)) + (marks (digitSum r.C (codeDigit F pol r) r.T w))) + (pair ((revNumFn F pol r.deg r.P r.T r.q (killArg r w)).take (r.P ^ r.T)) + (pair (randFn r w) (readFn w)))) + +theorem keyFn_mem_FP : keyFn F pol r ∈ FP := by + refine Cobham.pairFn_mem_FP (Cobham.pairFn_mem_FP (stepsFn_mem_FP r) (coinFn_mem_FP r)) + (Cobham.pairFn_mem_FP + (Cobham.pairFn_mem_FP (marks_mem_FP (digitSum_mem_FP (parDigit_mem_FP F pol r) r.T)) + (marks_mem_FP (digitSum_mem_FP (codeDigit_mem_FP F pol r) r.T))) + (Cobham.pairFn_mem_FP ?_ (Cobham.pairFn_mem_FP (randFn_mem_FP r) readFn_mem_FP))) + have hrev : (fun w : List Bool => revNumFn F pol r.deg r.P r.T r.q (killArg r w)) ∈ FP := + mem_FP_of_eq (mem_FP_comp (killArg_mem_FP r) + (revNumFn_mem_FP F pol r.deg r.P r.T r.q)) fun _ => rfl + have := Cobham.takeLenFn_mem_FP (constFn_mem_FP (List.replicate (r.P ^ r.T) true)) hrev + refine mem_FP_of_eq this fun w => ?_ + rw [List.length_replicate] + +/-! ### What the blocks read -/ + +/-- **The blocks split an edge number.** -/ +theorem blocks_eq (hD : 0 < r.cD) (hZ : 0 < r.cZ) (g : List Bool) (a b c d : ℕ) + (hb : b < r.cD) (hc : c < r.cZ) (hd : d < 22) : + testFn r (pair g (List.replicate (((a * r.cD + b) * r.cZ + c) * 22 + d) true)) + = List.replicate (a * r.cD + b) true + ∧ vertFn r (pair g (List.replicate (((a * r.cD + b) * r.cZ + c) * 22 + d) true)) + = List.replicate a true + ∧ dartFn r (pair g (List.replicate (((a * r.cD + b) * r.cZ + c) * 22 + d) true)) + = List.replicate b true + ∧ randFn r (pair g (List.replicate (((a * r.cD + b) * r.cZ + c) * 22 + d) true)) + = List.replicate c true + ∧ readFn (pair g (List.replicate (((a * r.cD + b) * r.cZ + c) * 22 + d) true)) + = List.replicate d true := by + have hre : ((a * r.cD + b) * r.cZ + c) * 22 + d + = (a * r.cD + b) * (r.cZ * 22) + (c * 22 + d) := by ring + obtain ⟨h1, h2, h3⟩ := MultiTest.split_mixed (a := a * r.cD + b) hc hd + rw [← hre] at h1 h2 h3 + have htest : testFn r (pair g (List.replicate (((a * r.cD + b) * r.cZ + c) * 22 + d) true)) + = List.replicate (a * r.cD + b) true := by + rw [testFn, pairSnd_pair, divC_eq (by positivity), List.length_replicate, h1] + refine ⟨htest, ?_, ?_, ?_, ?_⟩ + · rw [vertFn, htest, divC_eq hD, List.length_replicate, Nat.add_comm, + Nat.add_mul_div_right _ _ hD, Nat.div_eq_of_lt hb, Nat.zero_add] + · rw [dartFn, htest, modC_eq hD, List.length_replicate, Nat.add_comm, + Nat.add_mul_mod_self_right, Nat.mod_eq_of_lt hb] + · rw [randFn, pairSnd_pair, modC_eq (by positivity), List.length_replicate, + divC_eq (by omega), List.length_replicate, h2] + · rw [readFn, pairSnd_pair, modC_eq (by omega), List.length_replicate, h3] + +/-- **The dart block splits into steps and coins.** -/ +theorem steps_coin_eq (hQ : 0 < r.cQ) (w : List Bool) (s t : ℕ) (ht : t < r.cQ) + (hdart : dartFn r w = List.replicate (s * r.cQ + t) true) : + stepsFn r w = List.replicate s true ∧ coinFn r w = List.replicate t true := by + constructor + · rw [stepsFn, hdart, divC_eq hQ, List.length_replicate, Nat.add_comm, + Nat.add_mul_div_right _ _ hQ, Nat.div_eq_of_lt ht, Nat.zero_add] + · rw [coinFn, hdart, modC_eq hQ, List.length_replicate, Nat.add_comm, + Nat.add_mul_mod_self_right, Nat.mod_eq_of_lt ht] + +/-- **The walk's input**, once the blocks are known. -/ +theorem walkArg_eq {w g : List Bool} {a s : ℕ} (hg : pairFst w = g) + (hv : vertFn r w = List.replicate a true) + (hs : stepsFn r w = List.replicate s true) : + walkArg r w = pair g (pair (List.replicate a true) (List.replicate s true)) := by + rw [walkArg, hg, hv, hs] + +/-- **The killed walk's input**, likewise. -/ +theorem killArg_eq {w g : List Bool} {a b : ℕ} (hg : pairFst w = g) + (hv : vertFn r w = List.replicate a true) + (hb : dartFn r w = List.replicate b true) : + killArg r w = pair g (pair (List.replicate a true) (List.replicate b true)) := by + rw [killArg, hg, hv, hb] + +/-! ### The walk, on encoded vertices -/ + +variable {α : Type} [Fintype α] [DecidableEq α] + +variable {F pol} in +/-- **The walk algorithm, run on a dart's own numbers.** -/ +theorem walkFn_enc (hd : 1 < F.deg) (G : ConstraintGraph α) {T q : ℕ} + (v : (G.preprocess (F.toFamily hd)).graph.V) + (x : (Fin T → (G.preprocess (F.toFamily hd)).graph.D) × (Fin T → Fin q)) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) + (j : ℕ) (hj : j ≤ (G.preprocess (F.toFamily hd)).graph.kLen x) : + walkFn F pol (F.toFamily hd).degree (G.preDeg (F.toFamily hd)) j + (pair (encGraph G) (pair (List.replicate (NumEnc.enc v) true) + (List.replicate (NumEnc.enc x.1) true))) + = List.replicate (NumEnc.enc ((G.preprocess (F.toFamily hd)).graph.walkAt + ((G.preprocess (F.toFamily hd)).graph.kLen x) v + ((G.preprocess (F.toFamily hd)).graph.kWalk x) j)) true := by + have hv : NumEnc.enc v < 2 * G.numEdges := by + have h := NumEnc.enc_lt v + rw [NumEnc.card_eq_fintype_card] at h + have horder : Fintype.card (G.preprocess (F.toFamily hd)).graph.V = 2 * G.numEdges := + G.order_preprocess (F.toFamily hd) + omega + have hpos : 0 < G.preDeg (F.toFamily hd) := G.preDeg_pos _ + have hle : (G.preprocess (F.toFamily hd)).graph.kLen x ≤ T := + (G.preprocess (F.toFamily hd)).graph.kLen_le x + have hpre : ∀ (k : ℕ) (hk : k < (G.preprocess (F.toFamily hd)).graph.kLen x), + (NumEnc.enc x.1 / G.preDeg (F.toFamily hd) ^ k) % G.preDeg (F.toFamily hd) + = NumEnc.enc ((G.preprocess (F.toFamily hd)).graph.kWalk x ⟨k, hk⟩) := by + intro k hk + rw [G.digit_enc (F.toFamily hd) x.1 hpos k (lt_of_lt_of_le hk hle), RegGraph.kWalk, + RegGraph.preWalk] + refine (walkFn_eq F pol hd G _ _ hv hpc hpe j).trans ?_ + exact congrArg (fun n => List.replicate n true) + (G.walkNum_eq (F.toFamily hd) v ((G.preprocess (F.toFamily hd)).graph.kWalk x) + (NumEnc.enc x.1) hpre hj) + +variable {F pol} in +/-- **The stopping block is the effective walk's length.** -/ +theorem stopBlk_eq (hd : 1 < F.deg) (G : ConstraintGraph α) (hq : 0 < r.q) + (v : (G.preprocess (F.toFamily hd)).graph.V) + (x : (Fin r.T → (G.preprocess (F.toFamily hd)).graph.D) × (Fin r.T → Fin r.q)) + {w : List Bool} (hg : pairFst w = encGraph G) + (hv : vertFn r w = List.replicate (NumEnc.enc v) true) + (hdart : dartFn r w = List.replicate (NumEnc.enc x) true) : + stopBlk r w = List.replicate ((G.preprocess (F.toFamily hd)).graph.kLen x) true := by + have hlt : NumEnc.enc x.2 < r.q ^ r.T := NumEnc.enc_lt x.2 + have hxenc : NumEnc.enc x = NumEnc.enc x.1 * r.q ^ r.T + NumEnc.enc x.2 := rfl + have hco : coinsOf r.q r.T (killArg r w) = List.replicate (NumEnc.enc x.2) true := by + rw [killArg_eq r hg hv hdart, coinsOf, pairSnd_pair, pairSnd_pair, + modC_eq (Nat.pow_pos hq), List.length_replicate, hxenc, Nat.add_comm, + Nat.add_mul_mod_self_right, Nat.mod_eq_of_lt hlt] + rw [stopBlk, stopFn_eq hq hco r.T 0, ← stopAtNum_eq_stopFromNum, stopAtNum_eq hq x.2] + rfl + +variable {F pol} in +/-- **The parity block is the parity tuple's number.** -/ +theorem parBlk_eq (hd : 1 < F.deg) (G : ConstraintGraph α) (hq : 0 < r.q) + (hdeg : r.deg = (F.toFamily hd).degree) (hP : r.P = G.preDeg (F.toFamily hd)) + (v : (G.preprocess (F.toFamily hd)).graph.V) + (x : (Fin r.T → (G.preprocess (F.toFamily hd)).graph.D) × (Fin r.T → Fin r.q)) + {w : List Bool} (hg : pairFst w = encGraph G) + (hv : vertFn r w = List.replicate (NumEnc.enc v) true) + (hs : stepsFn r w = List.replicate (NumEnc.enc x.1) true) + (hdart : dartFn r w = List.replicate (NumEnc.enc x) true) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) + (B : ℕ) (z : Cube (ROf B)) (i : ReadIdx) : + marks (digitSum 2 (parDigit F pol r) r.T w) + = List.replicate (NumEnc.enc (stepKeyOf G (F.toFamily hd) v x B z i).par) true := by + have hstop := stopBlk_eq r hd G hq v x hg hv hdart + rw [marks_eq] + refine congrArg (List.replicate · true) ?_ + refine length_digitSum_eq_enc (X := Fin 2) + (stepKeyOf G (F.toFamily hd) v x B z i).par _ w fun j hj => ?_ + rw [parDigit, hstop, hdeg, hP] + by_cases hjk : j < (G.preprocess (F.toFamily hd)).graph.kLen x + · rw [ifLtLen_pos (by simpa using hjk), walkArg_eq r hg hv hs, + walkFn_enc hd G v x hpc hpe j (le_of_lt hjk), modC_eq (by omega), + List.length_replicate, List.length_replicate] + show _ = NumEnc.enc (StepKey.par _ _) + rw [stepKeyOf] + simp only [StepKey.par] + rw [dif_pos hjk] + rfl + · rw [ifLtLen_neg (by simpa using hjk), List.length_nil] + show _ = NumEnc.enc (StepKey.par _ _) + rw [stepKeyOf] + simp only [StepKey.par] + rw [dif_neg hjk] + rfl + +omit [Fintype α] [DecidableEq α] in +theorem enc_halfEdge_div_two (G : ConstraintGraph α) (p : G.HalfEdge) : + NumEnc.enc p / 2 = p.1.val := by + have hcode : NumEnc.enc p = 2 * p.1.val + (if p.2 then 0 else 1) := by + rw [ConstraintGraph.enc_halfEdge, ConstraintGraph.halfCode] + rw [hcode] + by_cases h : p.2 = true + · rw [if_pos h] + omega + · rw [if_neg h] + omega + +variable {F pol} in +/-- **The code block is the code tuple's number.** -/ +theorem codeBlk_eq (hd : 1 < F.deg) (G : ConstraintGraph α) (hq : 0 < r.q) + (hdeg : r.deg = (F.toFamily hd).degree) (hP : r.P = G.preDeg (F.toFamily hd)) + (hC : r.C = Fintype.card (α → α → Bool)) + (v : (G.preprocess (F.toFamily hd)).graph.V) + (x : (Fin r.T → (G.preprocess (F.toFamily hd)).graph.D) × (Fin r.T → Fin r.q)) + {w : List Bool} (hg : pairFst w = encGraph G) + (hv : vertFn r w = List.replicate (NumEnc.enc v) true) + (hs : stepsFn r w = List.replicate (NumEnc.enc x.1) true) + (hdart : dartFn r w = List.replicate (NumEnc.enc x) true) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) + (B : ℕ) (z : Cube (ROf B)) (i : ReadIdx) : + marks (digitSum r.C (codeDigit F pol r) r.T w) + = List.replicate (NumEnc.enc (stepKeyOf G (F.toFamily hd) v x B z i).code) true := by + have hstop := stopBlk_eq r hd G hq v x hg hv hdart + rw [marks_eq] + refine congrArg (List.replicate · true) ?_ + rw [hC] + refine length_digitSum_eq_enc (X := Fin (Fintype.card (α → α → Bool))) + (stepKeyOf G (F.toFamily hd) v x B z i).code _ w fun j hj => ?_ + rw [codeDigit, hstop, hdeg, hP] + by_cases hjk : j < (G.preprocess (F.toFamily hd)).graph.kLen x + · rw [ifLtLen_pos (by simpa using hjk), hg, walkArg_eq r hg hv hs, + walkFn_enc hd G v x hpc hpe j (le_of_lt hjk), divC_eq (by omega), + List.length_take] + simp only [List.length_replicate] + have henc : NumEnc.enc ((G.preprocess (F.toFamily hd)).graph.walkAt + ((G.preprocess (F.toFamily hd)).graph.kLen x) v + ((G.preprocess (F.toFamily hd)).graph.kWalk x) j) < 2 * G.numEdges := by + have h := NumEnc.enc_lt ((G.preprocess (F.toFamily hd)).graph.walkAt + ((G.preprocess (F.toFamily hd)).graph.kLen x) v + ((G.preprocess (F.toFamily hd)).graph.kWalk x) j) + rw [NumEnc.card_eq_fintype_card] at h + have horder : Fintype.card (G.preprocess (F.toFamily hd)).graph.V = 2 * G.numEdges := + G.order_preprocess (F.toFamily hd) + omega + have hidx : NumEnc.enc ((G.preprocess (F.toFamily hd)).graph.walkAt + ((G.preprocess (F.toFamily hd)).graph.kLen x) v + ((G.preprocess (F.toFamily hd)).graph.kWalk x) j) / 2 < G.numEdges := by omega + have hgc := gCode_encGraph G _ hidx + rw [gCode] at hgc + rw [hgc] + have hlt : codeOfRel (G.rel ⟨NumEnc.enc ((G.preprocess (F.toFamily hd)).graph.walkAt + ((G.preprocess (F.toFamily hd)).graph.kLen x) v + ((G.preprocess (F.toFamily hd)).graph.kWalk x) j) / 2, hidx⟩) < r.C := by + rw [hC] + exact codeOfRel_lt _ + rw [Nat.min_eq_right (le_of_lt hlt)] + show _ = NumEnc.enc (StepKey.code _ _) + rw [stepKeyOf] + simp only [StepKey.code] + rw [dif_pos hjk] + have hfin : (⟨NumEnc.enc ((G.preprocess (F.toFamily hd)).graph.walkAt + ((G.preprocess (F.toFamily hd)).graph.kLen x) v + ((G.preprocess (F.toFamily hd)).graph.kWalk x) j) / 2, hidx⟩ : Fin G.numEdges) + = ((G.preprocess (F.toFamily hd)).graph.walkAt + ((G.preprocess (F.toFamily hd)).graph.kLen x) v + ((G.preprocess (F.toFamily hd)).graph.kWalk x) j).1 := + Fin.ext (enc_halfEdge_div_two G _) + rw [hfin] + rfl + · rw [ifLtLen_neg (by simpa using hjk), List.length_nil] + show _ = NumEnc.enc (StepKey.code _ _) + rw [stepKeyOf] + simp only [StepKey.code] + rw [dif_neg hjk] + rfl + +variable {F pol} in +/-- **The return-dart block is the return tuple's number.** -/ +theorem revBlk_eq (hd : 1 < F.deg) (G : ConstraintGraph α) (hq : 0 < r.q) + (hdeg : r.deg = (F.toFamily hd).degree) (hP : r.P = G.preDeg (F.toFamily hd)) + (v : (G.preprocess (F.toFamily hd)).graph.V) + (x : (Fin r.T → (G.preprocess (F.toFamily hd)).graph.D) × (Fin r.T → Fin r.q)) + {w : List Bool} (hg : pairFst w = encGraph G) + (hv : vertFn r w = List.replicate (NumEnc.enc v) true) + (hdart : dartFn r w = List.replicate (NumEnc.enc x) true) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) + (B : ℕ) (z : Cube (ROf B)) (i : ReadIdx) : + (revNumFn F pol r.deg r.P r.T r.q (killArg r w)).take (r.P ^ r.T) + = List.replicate (NumEnc.enc (stepKeyOf G (F.toFamily hd) v x B z i).rev) true := by + have henc : NumEnc.enc v < 2 * G.numEdges := by + have h := NumEnc.enc_lt v + rw [NumEnc.card_eq_fintype_card] at h + have horder : Fintype.card (G.preprocess (F.toFamily hd)).graph.V = 2 * G.numEdges := + G.order_preprocess (F.toFamily hd) + omega + have hclt : NumEnc.enc x.2 < r.q ^ r.T := NumEnc.enc_lt x.2 + have hxenc : NumEnc.enc x = NumEnc.enc x.1 * r.q ^ r.T + NumEnc.enc x.2 := rfl + have hrev : NumEnc.enc ((G.preprocess (F.toFamily hd)).graph.killedRev v x.1 x.2) + < r.P ^ r.T := by + have h := NumEnc.enc_lt ((G.preprocess (F.toFamily hd)).graph.killedRev v x.1 x.2) + have hcard : NumEnc.card (Fin r.T → (G.preprocess (F.toFamily hd)).graph.D) + = G.preDeg (F.toFamily hd) ^ r.T := rfl + rw [hcard, ← hP] at h + exact h + rw [killArg_eq r hg hv hdart, hdeg, hP, hxenc, + revNumFn_eq hd G r.T r.q (NumEnc.enc v) (NumEnc.enc x.1) (NumEnc.enc x.2) hq henc + hclt hpc hpe] + have hknum : G.killedRevNum (F.toFamily hd) r.T r.q (NumEnc.enc v) (NumEnc.enc x.1) + (NumEnc.enc x.2) + = NumEnc.enc ((G.preprocess (F.toFamily hd)).graph.killedRev v x.1 x.2) := + G.killedRevNum_eq (F.toFamily hd) hq (G.preDeg_pos _) v x.1 x.2 + rw [hknum, List.take_replicate] + show List.replicate _ true = List.replicate (NumEnc.enc (StepKey.rev _)) true + rw [stepKeyOf] + simp only [StepKey.rev] + rw [← hP, Nat.min_eq_right (le_of_lt hrev)] + rfl + +variable {F pol} in +/-- **The algorithm writes out the walk's own data.** -/ +theorem keyFn_eq (hd : 1 < F.deg) (G : ConstraintGraph α) (hq : 0 < r.q) + (hdeg : r.deg = (F.toFamily hd).degree) (hP : r.P = G.preDeg (F.toFamily hd)) + (hC : r.C = Fintype.card (α → α → Bool)) (hZ : 0 < r.cZ) + (v : (G.preprocess (F.toFamily hd)).graph.V) + (x : (Fin r.T → (G.preprocess (F.toFamily hd)).graph.D) × (Fin r.T → Fin r.q)) + {B : ℕ} (z : Cube (ROf B)) (i : ReadIdx) + (hcZ : r.cZ = NumEnc.card (Cube (ROf B))) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) : + keyFn F pol r (pair (encGraph G) (List.replicate + (((NumEnc.enc v * r.cD + NumEnc.enc x) * r.cZ + NumEnc.enc z) * 22 + NumEnc.enc i) true)) + = packKey (stepKeyOf G (F.toFamily hd) v x B z i) := by + have hxlt : NumEnc.enc x < r.cD := by + have h := NumEnc.enc_lt x + have hcard : NumEnc.card ((Fin r.T → (G.preprocess (F.toFamily hd)).graph.D) + × (Fin r.T → Fin r.q)) = G.preDeg (F.toFamily hd) ^ r.T * r.q ^ r.T := rfl + rw [hcard] at h + rw [Round.cD, hP] + exact h + have hzlt : NumEnc.enc z < r.cZ := by rw [hcZ]; exact NumEnc.enc_lt z + have hilt : NumEnc.enc i < 22 := NumEnc.enc_lt i + obtain ⟨-, hv, hdart, hrand, hread⟩ := + blocks_eq r (by + rw [Round.cD, hP] + exact Nat.mul_pos (Nat.pow_pos (G.preDeg_pos _)) (Nat.pow_pos hq)) hZ (encGraph G) + (NumEnc.enc v) (NumEnc.enc x) (NumEnc.enc z) (NumEnc.enc i) hxlt hzlt hilt + have hclt : NumEnc.enc x.2 < r.cQ := NumEnc.enc_lt x.2 + have hxenc : NumEnc.enc x = NumEnc.enc x.1 * r.cQ + NumEnc.enc x.2 := rfl + obtain ⟨hs, hc⟩ := steps_coin_eq r (Nat.pow_pos hq) _ (NumEnc.enc x.1) + (NumEnc.enc x.2) hclt (by rw [hdart, hxenc]) + have hg : pairFst (pair (encGraph G) (List.replicate + (((NumEnc.enc v * r.cD + NumEnc.enc x) * r.cZ + NumEnc.enc z) * 22 + NumEnc.enc i) true)) + = encGraph G := pairFst_pair _ _ + rw [keyFn, hs, hc, hrand, hread, + parBlk_eq r hd G hq hdeg hP v x hg hv hs hdart hpc hpe B z i, + codeBlk_eq r hd G hq hdeg hP hC v x hg hv hs hdart hpc hpe B z i, + revBlk_eq r hd G hq hdeg hP v x hg hv hdart hpc hpe B z i, packKey] + rfl + +/-- A bound on the length of an edge's data. -/ +def keyBound : ℕ := + 2 * (2 * r.cD + 2 + r.cQ) + 2 + + (2 * (2 * (r.T * (1 * 2 ^ r.T)) + 2 + r.T * (r.C * r.C ^ r.T)) + 2 + + (2 * r.P ^ r.T + 2 + (2 * r.cZ + 2 + 22))) + +theorem keyFn_length_le (hQ : 0 < r.cQ) (hD : 0 < r.cD) (hZ : 0 < r.cZ) (hC : 0 < r.C) + (w : List Bool) : + (keyFn F pol r w).length ≤ keyBound r := by + have hdart : (dartFn r w).length < r.cD := by + rw [dartFn, modC_eq hD, List.length_replicate] + exact Nat.mod_lt _ hD + have hsteps : (stepsFn r w).length ≤ r.cD := by + rw [stepsFn, divC_eq hQ, List.length_replicate] + exact le_trans (Nat.div_le_self _ _) (le_of_lt hdart) + have hcoin : (coinFn r w).length < r.cQ := by + rw [coinFn, modC_eq hQ, List.length_replicate] + exact Nat.mod_lt _ hQ + have hrand : (randFn r w).length ≤ r.cZ := by + rw [randFn, divC_eq (by omega), List.length_replicate, modC_eq (by positivity), + List.length_replicate] + have hlt : (pairSnd w).length % (r.cZ * 22) < r.cZ * 22 := + Nat.mod_lt _ (by positivity) + exact Nat.div_le_of_le_mul (by omega) + have hread : (readFn w).length ≤ 22 := by + rw [readFn, modC_eq (by omega), List.length_replicate] + exact le_of_lt (Nat.mod_lt _ (by omega)) + have hpar : (digitSum 2 (parDigit F pol r) r.T w).length ≤ r.T * (1 * 2 ^ r.T) := by + refine length_digitSum_le (by omega) (fun j u => ?_) r.T w + rw [parDigit] + by_cases h : (List.replicate j true).length < (stopBlk r u).length + · rw [ifLtLen_pos h, modC_eq (by omega), List.length_replicate] + omega + · rw [ifLtLen_neg h] + simp + have hcode : (digitSum r.C (codeDigit F pol r) r.T w).length ≤ r.T * (r.C * r.C ^ r.T) := by + refine length_digitSum_le hC (fun j u => ?_) r.T w + rw [codeDigit] + by_cases h : (List.replicate j true).length < (stopBlk r u).length + · rw [ifLtLen_pos h] + exact le_trans (List.length_take_le _ _) (by simp) + · rw [ifLtLen_neg h] + simp + have hrevb : ((revNumFn F pol r.deg r.P r.T r.q (killArg r w)).take (r.P ^ r.T)).length + ≤ r.P ^ r.T := by + exact le_trans (List.length_take_le _ _) (by simp) + rw [keyFn, keyBound] + simp only [pair_length, marks_eq, List.length_replicate] + omega + +/-! ### The cube and the code, in polynomial time -/ + +variable {E : ExpanderFamily} {B : ℕ} + +/-- The cube a composed edge's second endpoint names. -/ +noncomputable def cubeFn + (dflt : StepKey E r.T r.q B (Fintype.card (α → α → Bool))) + (encβ : (PreWalk E r.T → α) → Cube B) (w : List Bool) : List Bool := + List.replicate (cubeOfKey encβ (keyOfString dflt (keyFn F pol r w))) true + +/-- The code of a composed edge's constraint. -/ +noncomputable def codeFn + (dflt : StepKey E r.T r.q B (Fintype.card (α → α → Bool))) + (encβ : (PreWalk E r.T → α) → Cube B) (w : List Bool) : List Bool := + List.replicate (codeOfKey encβ (keyOfString dflt (keyFn F pol r w))) true + +theorem cubeFn_mem_FP (hQ : 0 < r.cQ) (hD : 0 < r.cD) (hZ : 0 < r.cZ) (hC : 0 < r.C) + (dflt : StepKey E r.T r.q B (Fintype.card (α → α → Bool))) + (encβ : (PreWalk E r.T → α) → Cube B) : cubeFn F pol r dflt encβ ∈ FP := + mem_FP_of_bounded_key (keyFn_mem_FP F pol r) (keyFn_length_le F pol r hQ hD hZ hC) + (fun s => List.replicate (cubeOfKey encβ (keyOfString dflt s)) true) + +theorem codeFn_mem_FP (hQ : 0 < r.cQ) (hD : 0 < r.cD) (hZ : 0 < r.cZ) (hC : 0 < r.C) + (dflt : StepKey E r.T r.q B (Fintype.card (α → α → Bool))) + (encβ : (PreWalk E r.T → α) → Cube B) : codeFn F pol r dflt encβ ∈ FP := + mem_FP_of_bounded_key (keyFn_mem_FP F pol r) (keyFn_length_le F pol r hQ hD hZ hC) + (fun s => List.replicate (codeOfKey encβ (keyOfString dflt s)) true) + +variable {F pol} in +/-- **The cube algorithm computes the composed edge's cube.** -/ +theorem cubeFn_eq (hd : 1 < F.deg) (G : ConstraintGraph α) (hq : 0 < r.q) + (hdeg : r.deg = (F.toFamily hd).degree) (hP : r.P = G.preDeg (F.toFamily hd)) + (hC : r.C = Fintype.card (α → α → Bool)) (hZ : 0 < r.cZ) + (v : (G.preprocess (F.toFamily hd)).graph.V) + (x : (Fin r.T → (G.preprocess (F.toFamily hd)).graph.D) × (Fin r.T → Fin r.q)) + (z : Cube (ROf B)) (i : ReadIdx) + (hcZ : r.cZ = NumEnc.card (Cube (ROf B))) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) + (dflt : StepKey (F.toFamily hd) r.T r.q B (Fintype.card (α → α → Bool))) + (encβ : (PreWalk (F.toFamily hd) r.T → α) → Cube B) : + cubeFn F pol r dflt encβ (pair (encGraph G) (List.replicate + (((NumEnc.enc v * r.cD + NumEnc.enc x) * r.cZ + NumEnc.enc z) * 22 + NumEnc.enc i) true)) + = List.replicate (((G.preprocess (F.toFamily hd)).killedPow r.q r.T hq).cubeNum + encβ (v, x) z i) true := by + rw [cubeFn, keyFn_eq r hd G hq hdeg hP hC hZ v x z i hcZ hpc hpe, keyOfString_packKey, + cubeOfKey_eq G (F.toFamily hd) hq v x z i encβ] + +variable {F pol} in +/-- **The cube algorithm**, with the dart given as one object. -/ +theorem cubeFn_eq' (hd : 1 < F.deg) (G : ConstraintGraph α) (hq : 0 < r.q) + (hdeg : r.deg = (F.toFamily hd).degree) (hP : r.P = G.preDeg (F.toFamily hd)) + (hC : r.C = Fintype.card (α → α → Bool)) (hZ : 0 < r.cZ) + (p : ((G.preprocess (F.toFamily hd)).killedPow r.q r.T hq).Dart) + (z : Cube (ROf B)) (i : ReadIdx) + (hcZ : r.cZ = NumEnc.card (Cube (ROf B))) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) + (dflt : StepKey (F.toFamily hd) r.T r.q B (Fintype.card (α → α → Bool))) + (encβ : (PreWalk (F.toFamily hd) r.T → α) → Cube B) : + cubeFn F pol r dflt encβ (pair (encGraph G) (List.replicate + (((NumEnc.enc p.1 * r.cD + NumEnc.enc p.2) * r.cZ + NumEnc.enc z) * 22 + + NumEnc.enc i) true)) + = List.replicate (((G.preprocess (F.toFamily hd)).killedPow r.q r.T hq).cubeNum + encβ p z i) true := by + obtain ⟨v, x⟩ := p + exact cubeFn_eq r hd G hq hdeg hP hC hZ v x z i hcZ hpc hpe dflt encβ + +variable {F pol} in +/-- **The code algorithm computes the composed edge's constraint.** -/ +theorem codeFn_eq (hd : 1 < F.deg) (G : ConstraintGraph α) (hq : 0 < r.q) + (hdeg : r.deg = (F.toFamily hd).degree) (hP : r.P = G.preDeg (F.toFamily hd)) + (hC : r.C = Fintype.card (α → α → Bool)) (hZ : 0 < r.cZ) + (v : (G.preprocess (F.toFamily hd)).graph.V) + (x : (Fin r.T → (G.preprocess (F.toFamily hd)).graph.D) × (Fin r.T → Fin r.q)) + (z : Cube (ROf B)) (i : ReadIdx) + (hcZ : r.cZ = NumEnc.card (Cube (ROf B))) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) + (dflt : StepKey (F.toFamily hd) r.T r.q B (Fintype.card (α → α → Bool))) + (encβ : (PreWalk (F.toFamily hd) r.T → α) → Cube B) : + codeFn F pol r dflt encβ (pair (encGraph G) (List.replicate + (((NumEnc.enc v * r.cD + NumEnc.enc x) * r.cZ + NumEnc.enc z) * 22 + NumEnc.enc i) true)) + = List.replicate (codeOfRel (MultiTest.relOfCheck + ((((G.preprocess (F.toFamily hd)).killedPow r.q r.T hq).compose encβ).check (v, x) z) + i)) true := by + rw [codeFn, keyFn_eq r hd G hq hdeg hP hC hZ v x z i hcZ hpc hpe, keyOfString_packKey, + codeOfKey_eq G (F.toFamily hd) hq v x z i encβ] + +variable {F pol} in +/-- **The code algorithm**, with the dart given as one object. -/ +theorem codeFn_eq' (hd : 1 < F.deg) (G : ConstraintGraph α) (hq : 0 < r.q) + (hdeg : r.deg = (F.toFamily hd).degree) (hP : r.P = G.preDeg (F.toFamily hd)) + (hC : r.C = Fintype.card (α → α → Bool)) (hZ : 0 < r.cZ) + (p : ((G.preprocess (F.toFamily hd)).killedPow r.q r.T hq).Dart) + (z : Cube (ROf B)) (i : ReadIdx) + (hcZ : r.cZ = NumEnc.card (Cube (ROf B))) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) + (dflt : StepKey (F.toFamily hd) r.T r.q B (Fintype.card (α → α → Bool))) + (encβ : (PreWalk (F.toFamily hd) r.T → α) → Cube B) : + codeFn F pol r dflt encβ (pair (encGraph G) (List.replicate + (((NumEnc.enc p.1 * r.cD + NumEnc.enc p.2) * r.cZ + NumEnc.enc z) * 22 + + NumEnc.enc i) true)) + = List.replicate (codeOfRel (MultiTest.relOfCheck + ((((G.preprocess (F.toFamily hd)).killedPow r.q r.T hq).compose encβ).check p z) + i)) true := by + obtain ⟨v, x⟩ := p + exact codeFn_eq r hd G hq hdeg hP hC hZ v x z i hcZ hpc hpe dflt encβ + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgKilled.lean b/Complexitylib/Classes/PCP/Internal/AlgKilled.lean new file mode 100644 index 00000000..f4d46e96 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgKilled.lean @@ -0,0 +1,543 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.PCP.Internal.AlgPreRot +public import Complexitylib.Classes.PCP.Internal.AlgWalk + +/-! +# The killed walk, as an algorithm + +The walk length and the killing denominator are constants of a round, so the +walk is a constant-depth unrolling of the preprocessed rotation map, and the +stopping index is chosen by a constant-depth chain of comparisons. + +## Main definitions + +- `Complexity.selectAt` — choose among constantly many functions by a unary key +- `Complexity.walkFn` — the vertex a walk reaches after a constant number of + steps + +## Main results + +- `Complexity.walkFn_mem_FP`, `Complexity.walkFn_eq` — it is an `FP` function, + and it computes `ConstraintGraph.walkNum` +-/ + +@[expose] public section + +namespace Complexity + +variable {α : Type} [Fintype α] [DecidableEq α] + +namespace ConstraintGraph + +variable (G : ConstraintGraph α) (E : ExpanderFamily) + +omit [Fintype α] [DecidableEq α] in +/-- Every code in a cloud is below twice the edge count. -/ +theorem mem_cloudCodes_lt {u : Fin G.numVerts} {c : ℕ} (h : c ∈ G.cloudCodes u) : + c < 2 * G.numEdges := by + obtain ⟨p, _, rfl⟩ := (G.mem_cloudCodes).mp h + exact halfCode_lt G p + +omit [Fintype α] in +/-- **The rotation map keeps a vertex number in range.** -/ +theorem preRotNum_fst_lt {v : ℕ} (hv : v < 2 * G.numEdges) (d : ℕ) : + (G.preRotNum E v d).1 < 2 * G.numEdges := by + classical + have horder : (G.reduce E).graph.order = 2 * G.numEdges := by + rw [graph_reduce, order_reduceGraph] + rw [preRotNum] + split + · exact hv + · split + · simp only + split <;> omega + · split + · simp only + rw [cloudStepN] + split + · split + · rw [cloudStepNum] + split + · simp only + exact G.mem_cloudCodes_lt (Finset.orderEmbOfFin_mem _ _ _) + · exact hv + · exact hv + · exact hv + · simp only + rw [expStepN] + split + · split + · rw [← horder] + exact Fin.isLt _ + · exact hv + · exact hv + +omit [Fintype α] in +/-- **A walk stays in range.** -/ +theorem walkNum_lt {v : ℕ} (hv : v < 2 * G.numEdges) (s : ℕ) : + ∀ k, G.walkNum E s k v < 2 * G.numEdges := by + intro k + induction k with + | zero => exact hv + | succ k ih => exact G.preRotNum_fst_lt E ih _ + +omit [Fintype α] in +/-- **The preprocessed graph has `2 + 2 · deg` darts at a vertex.** -/ +theorem preDeg_eq : G.preDeg E = 2 + 2 * E.degree := by + rw [preDeg, NumEnc.card_eq_fintype_card] + show Fintype.card (Unit ⊕ (Option (Fin E.degree) ⊕ Fin E.degree)) = _ + simp + omega + +omit [Fintype α] in +theorem preDeg_pos : 0 < G.preDeg E := by rw [G.preDeg_eq E]; omega + +end ConstraintGraph + +/-! ### Choosing by a unary key -/ + +/-- Choose among `n + 1` functions by a unary key. -/ +noncomputable def selectAt (f : ℕ → List Bool → List Bool) (key : List Bool → List Bool) : + ℕ → List Bool → List Bool + | 0, z => f 0 z + | n + 1, z => + ifEqLen (key z) (List.replicate (n + 1) true) (f (n + 1) z) (selectAt f key n z) + +theorem selectAt_mem_FP {f : ℕ → List Bool → List Bool} {key : List Bool → List Bool} + (hf : ∀ k, f k ∈ FP) (hkey : key ∈ FP) : ∀ n, selectAt f key n ∈ FP := by + intro n + induction n with + | zero => exact hf 0 + | succ n ih => + refine mem_FP_of_eq (ifEqLen_mem_FP hkey + (constFn_mem_FP (List.replicate (n + 1) true)) (hf (n + 1)) ih) fun w => ?_ + rw [selectAt] + +theorem selectAt_eq {f : ℕ → List Bool → List Bool} {key : List Bool → List Bool} + {z : List Bool} {m : ℕ} (hkey : key z = List.replicate m true) : + ∀ {n : ℕ}, m ≤ n → selectAt f key n z = f m z := by + intro n + induction n with + | zero => + intro h + rw [selectAt, Nat.le_zero.mp h] + | succ n ih => + intro h + rw [selectAt] + by_cases hm : m = n + 1 + · subst hm + rw [ifEqLen_pos (by simp [hkey])] + · rw [ifEqLen_neg (by + rw [hkey, List.length_replicate, List.length_replicate] + exact hm), ih (by omega)] + +/-! ### Walking -/ + +variable (F : FinBase) (pol : Polynomial ℕ) + +/-- The vertex a walk reaches after `k` steps, on +`pair (graph) (pair (unary vertex) (unary steps))`. -/ +noncomputable def walkFn (deg P : ℕ) : ℕ → List Bool → List Bool + | 0, w => pairFst (pairSnd w) + | k + 1, w => + pairFst (preRotFn F pol deg + (pair (pairFst w) + (pair (walkFn deg P k w) + (modC P (divC (P ^ k) (pairSnd (pairSnd w))))))) + +theorem walkFn_mem_FP (deg P : ℕ) : ∀ k, walkFn F pol deg P k ∈ FP := by + intro k + induction k with + | zero => + refine mem_FP_of_eq (mem_FP_comp Cobham.sndBlock_mem_FP Cobham.fstBlock_mem_FP) + fun w => ?_ + rw [Function.comp_apply, walkFn] + | succ k ih => + have hs : (fun w : List Bool => pairSnd (pairSnd w)) ∈ FP := + mem_FP_comp Cobham.sndBlock_mem_FP Cobham.sndBlock_mem_FP + have h := mem_FP_comp (Cobham.pairFn_mem_FP Cobham.fstBlock_mem_FP + (Cobham.pairFn_mem_FP ih (modC_mem_FP (divC_mem_FP hs (P ^ k)) P))) + (mem_FP_comp (preRotFn_mem_FP F pol deg) Cobham.fstBlock_mem_FP) + refine mem_FP_of_eq h fun w => ?_ + simp only [Function.comp_apply] + rw [walkFn] + +/-- **The walk algorithm computes the walk.** -/ +theorem walkFn_eq (hd : 1 < F.deg) (G : ConstraintGraph α) (v s : ℕ) + (hv : v < 2 * G.numEdges) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) : + ∀ k, walkFn F pol (F.toFamily hd).degree (G.preDeg (F.toFamily hd)) k + (pair (encGraph G) (pair (List.replicate v true) (List.replicate s true))) + = List.replicate (G.walkNum (F.toFamily hd) s k v) true := by + have hPpos : 0 < G.preDeg (F.toFamily hd) := G.preDeg_pos _ + intro k + induction k with + | zero => + rw [walkFn, pairSnd_pair, pairFst_pair] + rfl + | succ k ih => + have hdig : modC (G.preDeg (F.toFamily hd)) + (divC (G.preDeg (F.toFamily hd) ^ k) (List.replicate s true)) + = List.replicate ((s / G.preDeg (F.toFamily hd) ^ k) + % G.preDeg (F.toFamily hd)) true := by + rw [divC_eq (Nat.pow_pos hPpos), List.length_replicate, + modC_eq hPpos, List.length_replicate] + have hdlt : (s / G.preDeg (F.toFamily hd) ^ k) % G.preDeg (F.toFamily hd) + < 2 + 2 * (F.toFamily hd).degree := by + rw [← G.preDeg_eq (F.toFamily hd)] + exact Nat.mod_lt _ hPpos + rw [walkFn, pairFst_pair, pairSnd_pair, pairSnd_pair, ih, hdig, + preRotFn_eq G F pol hd _ _ (G.walkNum_lt _ hv s k) hdlt hpc hpe, + pairFst_pair, ConstraintGraph.walkNum] + +/-! ### Where the walk stops -/ + +theorem findIdx_map {β γ : Type} (f : β → γ) (p : γ → Bool) (l : List β) : + (l.map f).findIdx p = l.findIdx (fun x => p (f x)) := by + induction l with + | nil => rfl + | cons a t ih => rw [List.map_cons, List.findIdx_cons, List.findIdx_cons, ih] + +/-- The first zero digit at or after `i`, among the next `n` digits. -/ +def stopFromNum (q c : ℕ) : ℕ → ℕ → ℕ + | i, 0 => i + | i, n + 1 => if (c / q ^ i) % q = 0 then i else stopFromNum q c (i + 1) n + +theorem stopFromNum_eq_findIdx (q c : ℕ) : ∀ (n i : ℕ), stopFromNum q c i n + = i + (List.finRange n).findIdx (fun j : Fin n => (c / q ^ (i + j.val)) % q == 0) := by + intro n + induction n with + | zero => + intro i + rw [stopFromNum, List.finRange_zero, List.findIdx_nil, Nat.add_zero] + | succ n ih => + intro i + rw [stopFromNum, List.finRange_succ, List.findIdx_cons, findIdx_map] + by_cases h : (c / q ^ i) % q = 0 + · rw [if_pos h] + simp [h] + · rw [if_neg h, ih (i + 1)] + have hcond : ((c / q ^ (i + (0 : Fin (n + 1)).val)) % q == 0) = false := by + simpa using h + rw [hcond] + have hbody : (List.finRange n).findIdx + (fun x : Fin n => (c / q ^ (i + (Fin.succ x).val)) % q == 0) + = (List.finRange n).findIdx + (fun j : Fin n => (c / q ^ (i + 1 + j.val)) % q == 0) := by + refine findIdx_congr fun x _ => ?_ + rw [Fin.val_succ, show i + (x.val + 1) = i + 1 + x.val by omega] + rw [hbody] + simp only [cond_false] + omega + +theorem stopAtNum_eq_stopFromNum (T q c : ℕ) : stopAtNum T q c = stopFromNum q c 0 T := by + rw [stopFromNum_eq_findIdx, stopAtNum, Nat.zero_add] + exact (findIdx_congr fun j _ => by rw [Nat.zero_add]).symm + +/-- Where the walk stops, from a unary reading `co` of the coins. -/ +noncomputable def stopFn (q : ℕ) (co : List Bool → List Bool) : + ℕ → ℕ → List Bool → List Bool + | i, 0, _ => List.replicate i true + | i, n + 1, z => + ifEqLen (modC q (divC (q ^ i) (co z))) [] (List.replicate i true) + (stopFn q co (i + 1) n z) + +theorem stopFn_mem_FP {q : ℕ} {co : List Bool → List Bool} (hco : co ∈ FP) : + ∀ (n i : ℕ), stopFn q co i n ∈ FP := by + intro n + induction n with + | zero => exact fun i => constFn_mem_FP _ + | succ n ih => + intro i + refine mem_FP_of_eq (ifEqLen_mem_FP (modC_mem_FP (divC_mem_FP hco (q ^ i)) q) + (constFn_mem_FP []) (constFn_mem_FP (List.replicate i true)) (ih (i + 1))) fun w => ?_ + rw [stopFn] + +/-- **The stopping algorithm finds the stopping index.** -/ +theorem stopFn_eq {q : ℕ} (hq : 0 < q) {co : List Bool → List Bool} {z : List Bool} {c : ℕ} + (hco : co z = List.replicate c true) : + ∀ (n i : ℕ), stopFn q co i n z = List.replicate (stopFromNum q c i n) true := by + intro n + induction n with + | zero => intro i; rw [stopFn, stopFromNum] + | succ n ih => + intro i + have hdig : modC q (divC (q ^ i) (co z)) = List.replicate ((c / q ^ i) % q) true := by + rw [hco, divC_eq (Nat.pow_pos hq), List.length_replicate, modC_eq hq, + List.length_replicate] + rw [stopFn, hdig, stopFromNum] + by_cases h : (c / q ^ i) % q = 0 + · rw [if_pos h, ifEqLen_pos (by simp [h])] + · rw [if_neg h, ifEqLen_neg (by simpa using h), ih (i + 1)] + +/-! ### The dart the walk comes back by -/ + +/-- The label the walk's `i`-th step points back along. -/ +noncomputable def backFn (deg P i : ℕ) (w : List Bool) : List Bool := + pairSnd (preRotFn F pol deg + (pair (pairFst w) + (pair (walkFn F pol deg P i w) + (modC P (divC (P ^ i) (pairSnd (pairSnd w))))))) + +theorem backFn_mem_FP (deg P i : ℕ) : backFn F pol deg P i ∈ FP := by + have hs : (fun w : List Bool => pairSnd (pairSnd w)) ∈ FP := + mem_FP_comp Cobham.sndBlock_mem_FP Cobham.sndBlock_mem_FP + have h := mem_FP_comp (Cobham.pairFn_mem_FP Cobham.fstBlock_mem_FP + (Cobham.pairFn_mem_FP (walkFn_mem_FP F pol deg P i) + (modC_mem_FP (divC_mem_FP hs (P ^ i)) P))) + (mem_FP_comp (preRotFn_mem_FP F pol deg) Cobham.sndBlock_mem_FP) + refine mem_FP_of_eq h fun w => ?_ + simp only [Function.comp_apply] + rw [backFn] + +variable {F pol} in +/-- **The back-label algorithm reads the label off the rotation map.** -/ +theorem backFn_eq (hd : 1 < F.deg) (G : ConstraintGraph α) (v s i : ℕ) + (hv : v < 2 * G.numEdges) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) : + backFn F pol (F.toFamily hd).degree (G.preDeg (F.toFamily hd)) i + (pair (encGraph G) (pair (List.replicate v true) (List.replicate s true))) + = List.replicate (G.preRotNum (F.toFamily hd) (G.walkNum (F.toFamily hd) s i v) + ((s / G.preDeg (F.toFamily hd) ^ i) % G.preDeg (F.toFamily hd))).2 true := by + have hPpos : 0 < G.preDeg (F.toFamily hd) := G.preDeg_pos _ + have hdig : modC (G.preDeg (F.toFamily hd)) + (divC (G.preDeg (F.toFamily hd) ^ i) (List.replicate s true)) + = List.replicate ((s / G.preDeg (F.toFamily hd) ^ i) + % G.preDeg (F.toFamily hd)) true := by + rw [divC_eq (Nat.pow_pos hPpos), List.length_replicate, modC_eq hPpos, + List.length_replicate] + have hdlt : (s / G.preDeg (F.toFamily hd) ^ i) % G.preDeg (F.toFamily hd) + < 2 + 2 * (F.toFamily hd).degree := by + rw [← G.preDeg_eq (F.toFamily hd)] + exact Nat.mod_lt _ hPpos + rw [backFn, pairFst_pair, pairSnd_pair, pairSnd_pair, + walkFn_eq F pol hd G v s hv hpc hpe, hdig, + preRotFn_eq G F pol hd _ _ (G.walkNum_lt _ hv s i) hdlt hpc hpe, pairSnd_pair] + +/-- The reversed dart's digits, for a fixed stopping index `k`, over the first +`n` places. -/ +noncomputable def revSum (deg P k : ℕ) : ℕ → List Bool → List Bool + | 0, _ => [] + | n + 1, w => + revSum deg P k n w ++ mulC (P ^ n) + (if n < k then backFn F pol deg P (k - 1 - n) w + else modC P (divC (P ^ n) (pairSnd (pairSnd w)))) + +theorem revSum_mem_FP (deg P k : ℕ) : ∀ n, revSum F pol deg P k n ∈ FP := by + intro n + induction n with + | zero => exact mem_FP_of_eq (constFn_mem_FP []) fun w => by rw [revSum] + | succ n ih => + have hs : (fun w : List Bool => pairSnd (pairSnd w)) ∈ FP := + mem_FP_comp Cobham.sndBlock_mem_FP Cobham.sndBlock_mem_FP + have hterm : (fun w : List Bool => + if n < k then backFn F pol deg P (k - 1 - n) w + else modC P (divC (P ^ n) (pairSnd (pairSnd w)))) ∈ FP := by + by_cases h : n < k + · simpa [h] using backFn_mem_FP F pol deg P (k - 1 - n) + · simpa [h] using modC_mem_FP (divC_mem_FP hs (P ^ n)) P + refine mem_FP_of_eq (Cobham.appendFn_mem_FP ih (mulC_mem_FP hterm (P ^ n))) fun w => ?_ + rw [revSum] + +variable {F pol} in +/-- **The digit sum has the reversed dart's number as its length.** -/ +theorem length_revSum (hd : 1 < F.deg) (G : ConstraintGraph α) (v s k : ℕ) + (hv : v < 2 * G.numEdges) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) : + ∀ n, (revSum F pol (F.toFamily hd).degree (G.preDeg (F.toFamily hd)) k n + (pair (encGraph G) (pair (List.replicate v true) (List.replicate s true)))).length + = ∑ j ∈ Finset.range n, + (if j < k then (G.preRotNum (F.toFamily hd) + (G.walkNum (F.toFamily hd) s (k - 1 - j) v) + ((s / G.preDeg (F.toFamily hd) ^ (k - 1 - j)) + % G.preDeg (F.toFamily hd))).2 + else (s / G.preDeg (F.toFamily hd) ^ j) % G.preDeg (F.toFamily hd)) + * G.preDeg (F.toFamily hd) ^ j := by + have hPpos : 0 < G.preDeg (F.toFamily hd) := G.preDeg_pos _ + intro n + induction n with + | zero => rw [revSum, Finset.range_zero, Finset.sum_empty, List.length_nil] + | succ n ih => + rw [revSum, List.length_append, ih, Finset.sum_range_succ, length_mulC] + congr 1 + congr 1 + by_cases h : n < k + · rw [if_pos h, if_pos h, backFn_eq hd G v s (k - 1 - n) hv hpc hpe, + List.length_replicate] + · rw [if_neg h, if_neg h, pairSnd_pair, pairSnd_pair, + divC_eq (Nat.pow_pos hPpos), List.length_replicate, modC_eq hPpos] + simp + +namespace ConstraintGraph + +variable (G : ConstraintGraph α) (E : ExpanderFamily) + +/-- The reversed dart's number, for a fixed stopping index. -/ +noncomputable def revAtNum (T k v s : ℕ) : ℕ := + ∑ j ∈ Finset.range T, + (if j < k then (G.preRotNum E (G.walkNum E s (k - 1 - j) v) + ((s / G.preDeg E ^ (k - 1 - j)) % G.preDeg E)).2 + else (s / G.preDeg E ^ j) % G.preDeg E) * G.preDeg E ^ j + +omit [Fintype α] in +theorem killedRevNum_eq_revAtNum (T q v s c : ℕ) : + G.killedRevNum E T q v s c = G.revAtNum E T (stopAtNum T q c) v s := rfl + +end ConstraintGraph + +/-! ### The powered graph's rotation map -/ + +/-- The coins of a killed dart. -/ +noncomputable def coinsOf (q T : ℕ) (z : List Bool) : List Bool := + modC (q ^ T) (pairSnd (pairSnd z)) + +/-- A killed dart's steps, in the walk's input format. -/ +noncomputable def toWalk (q T : ℕ) (z : List Bool) : List Bool := + pair (pairFst z) + (pair (pairFst (pairSnd z)) + (divC (q ^ T) (pairSnd (pairSnd z)))) + +theorem coinsOf_mem_FP (q T : ℕ) : coinsOf q T ∈ FP := + modC_mem_FP (mem_FP_comp Cobham.sndBlock_mem_FP Cobham.sndBlock_mem_FP) _ + +theorem toWalk_mem_FP (q T : ℕ) : toWalk q T ∈ FP := + Cobham.pairFn_mem_FP Cobham.fstBlock_mem_FP + (Cobham.pairFn_mem_FP (mem_FP_comp Cobham.sndBlock_mem_FP Cobham.fstBlock_mem_FP) + (divC_mem_FP (mem_FP_comp Cobham.sndBlock_mem_FP Cobham.sndBlock_mem_FP) _)) + +/-- **The dart a killed walk comes back by**, on `pair (graph) (pair (unary +vertex) (unary dart))`. -/ +noncomputable def revNumFn (deg P T q : ℕ) (z : List Bool) : List Bool := + selectAt (fun k w => marks (revSum F pol deg P k T (toWalk q T w))) + (fun w => stopFn q (coinsOf q T) 0 T w) T z + +theorem revNumFn_mem_FP (deg P T q : ℕ) : revNumFn F pol deg P T q ∈ FP := + selectAt_mem_FP + (fun k => marks_mem_FP (mem_FP_of_eq + (mem_FP_comp (toWalk_mem_FP q T) (revSum_mem_FP F pol deg P k T)) fun _ => rfl)) + (stopFn_mem_FP (coinsOf_mem_FP q T) T 0) T + +/-- **The powered graph's rotation map**, on `pair (graph) (pair (unary vertex) +(unary dart))`: walk to the end, come back by the reversed labels, and keep the +coins. -/ +noncomputable def killedRotFn (deg P T q : ℕ) (z : List Bool) : List Bool := + pair + (selectAt (fun k w => walkFn F pol deg P k (toWalk q T w)) + (fun w => stopFn q (coinsOf q T) 0 T w) T z) + (marks (mulC (q ^ T) (revNumFn F pol deg P T q z)) ++ coinsOf q T z) + +theorem killedRotFn_mem_FP (deg P T q : ℕ) : killedRotFn F pol deg P T q ∈ FP := by + have hkey : (fun w : List Bool => stopFn q (coinsOf q T) 0 T w) ∈ FP := + stopFn_mem_FP (coinsOf_mem_FP q T) T 0 + have hwalk : ∀ k, (fun w : List Bool => walkFn F pol deg P k (toWalk q T w)) ∈ FP := + fun k => mem_FP_of_eq (mem_FP_comp (toWalk_mem_FP q T) (walkFn_mem_FP F pol deg P k)) + fun _ => rfl + have hrev : ∀ k, (fun w : List Bool => + marks (revSum F pol deg P k T (toWalk q T w))) ∈ FP := + fun k => marks_mem_FP (mem_FP_of_eq + (mem_FP_comp (toWalk_mem_FP q T) (revSum_mem_FP F pol deg P k T)) fun _ => rfl) + refine mem_FP_of_eq (Cobham.pairFn_mem_FP (selectAt_mem_FP hwalk hkey T) + (Cobham.appendFn_mem_FP + (marks_mem_FP (mulC_mem_FP (selectAt_mem_FP hrev hkey T) (q ^ T))) + (coinsOf_mem_FP q T))) fun w => ?_ + rw [killedRotFn, revNumFn] + +variable {F pol} in +theorem marks_revSum_eq (hd : 1 < F.deg) (G : ConstraintGraph α) (T v s k : ℕ) + (hv : v < 2 * G.numEdges) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) : + marks (revSum F pol (F.toFamily hd).degree (G.preDeg (F.toFamily hd)) k T + (pair (encGraph G) (pair (List.replicate v true) (List.replicate s true)))) + = List.replicate (G.revAtNum (F.toFamily hd) T k v s) true := by + rw [marks_eq, length_revSum hd G v s k hv hpc hpe, ConstraintGraph.revAtNum] + +variable {F pol} in +/-- **The return-dart algorithm computes the dart the walk comes back by.** -/ +theorem revNumFn_eq (hd : 1 < F.deg) (G : ConstraintGraph α) (T q v s c : ℕ) + (hq : 0 < q) (hv : v < 2 * G.numEdges) (hc : c < q ^ T) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) : + revNumFn F pol (F.toFamily hd).degree (G.preDeg (F.toFamily hd)) T q + (pair (encGraph G) (pair (List.replicate v true) + (List.replicate (s * q ^ T + c) true))) + = List.replicate (G.killedRevNum (F.toFamily hd) T q v s c) true := by + have hqT : 0 < q ^ T := Nat.pow_pos hq + have hdiv : (s * q ^ T + c) / q ^ T = s := by + rw [Nat.add_comm, Nat.add_mul_div_right _ _ hqT, Nat.div_eq_of_lt hc, Nat.zero_add] + have hmod : (s * q ^ T + c) % q ^ T = c := by + rw [Nat.add_comm, Nat.add_mul_mod_self_right, Nat.mod_eq_of_lt hc] + have hco : coinsOf q T (pair (encGraph G) (pair (List.replicate v true) + (List.replicate (s * q ^ T + c) true))) = List.replicate c true := by + rw [coinsOf, pairSnd_pair, pairSnd_pair, modC_eq hqT, + List.length_replicate, hmod] + have htw : toWalk q T (pair (encGraph G) (pair (List.replicate v true) + (List.replicate (s * q ^ T + c) true))) + = pair (encGraph G) (pair (List.replicate v true) (List.replicate s true)) := by + rw [toWalk, pairFst_pair, pairSnd_pair, pairFst_pair, + pairSnd_pair, divC_eq hqT, List.length_replicate, hdiv] + have hstop : stopFn q (coinsOf q T) 0 T (pair (encGraph G) (pair (List.replicate v true) + (List.replicate (s * q ^ T + c) true))) = List.replicate (stopAtNum T q c) true := by + rw [stopFn_eq hq hco T 0, ← stopAtNum_eq_stopFromNum] + have hle : stopAtNum T q c ≤ T := by + have h : (List.finRange T).findIdx (fun j : Fin T => (c / q ^ j.val) % q == 0) + ≤ (List.finRange T).length := List.findIdx_le_length + rwa [List.length_finRange] at h + rw [revNumFn, selectAt_eq hstop hle, htw, marks_revSum_eq hd G T v s _ hv hpc hpe, + G.killedRevNum_eq_revAtNum] + +variable {F pol} in +/-- **The rotation algorithm runs the powered graph's rotation map.** -/ +theorem killedRotFn_eq (hd : 1 < F.deg) (G : ConstraintGraph α) (T q v s c : ℕ) + (hq : 0 < q) (hv : v < 2 * G.numEdges) (hc : c < q ^ T) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) : + killedRotFn F pol (F.toFamily hd).degree (G.preDeg (F.toFamily hd)) T q + (pair (encGraph G) (pair (List.replicate v true) + (List.replicate (s * q ^ T + c) true))) + = pair (List.replicate (G.killedRotNum (F.toFamily hd) T q v s c).1 true) + (List.replicate (G.killedRotNum (F.toFamily hd) T q v s c).2 true) := by + have hqT : 0 < q ^ T := Nat.pow_pos hq + have hdiv : (s * q ^ T + c) / q ^ T = s := by + rw [Nat.add_comm, Nat.add_mul_div_right _ _ hqT, Nat.div_eq_of_lt hc, Nat.zero_add] + have hmod : (s * q ^ T + c) % q ^ T = c := by + rw [Nat.add_comm, Nat.add_mul_mod_self_right, Nat.mod_eq_of_lt hc] + have hco : coinsOf q T (pair (encGraph G) (pair (List.replicate v true) + (List.replicate (s * q ^ T + c) true))) = List.replicate c true := by + rw [coinsOf, pairSnd_pair, pairSnd_pair, modC_eq hqT, + List.length_replicate, hmod] + have htw : toWalk q T (pair (encGraph G) (pair (List.replicate v true) + (List.replicate (s * q ^ T + c) true))) + = pair (encGraph G) (pair (List.replicate v true) (List.replicate s true)) := by + rw [toWalk, pairFst_pair, pairSnd_pair, pairFst_pair, + pairSnd_pair, divC_eq hqT, List.length_replicate, hdiv] + have hstop : stopFn q (coinsOf q T) 0 T (pair (encGraph G) (pair (List.replicate v true) + (List.replicate (s * q ^ T + c) true))) = List.replicate (stopAtNum T q c) true := by + rw [stopFn_eq hq hco T 0, ← stopAtNum_eq_stopFromNum] + have hle : stopAtNum T q c ≤ T := by + have h : (List.finRange T).findIdx (fun j : Fin T => (c / q ^ j.val) % q == 0) + ≤ (List.finRange T).length := List.findIdx_le_length + rwa [List.length_finRange] at h + rw [killedRotFn, revNumFn, selectAt_eq hstop hle, selectAt_eq hstop hle, htw, + walkFn_eq F pol hd G v s hv hpc hpe, marks_revSum_eq hd G T v s _ hv hpc hpe, + marks_eq, length_mulC, List.length_replicate, hco, ConstraintGraph.killedRotNum] + dsimp only + refine congrArg (pair _) ?_ + rw [← List.replicate_add, G.killedRevNum_eq_revAtNum] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgLevel.lean b/Complexitylib/Classes/PCP/Internal/AlgLevel.lean new file mode 100644 index 00000000..1be2f2da --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgLevel.lean @@ -0,0 +1,318 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.PolyRuler +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.PCP.Internal.TowerFin +public import Complexitylib.Classes.PCP.Internal.Materialize + +/-! +# Finding the tower level + +The expander family answers a request for `n` vertices with the first tower +member of at least `2 n` of them. An algorithm finds that member by counting: it +walks up the tower, multiplying the size by `deg ^ 4` at each level, and stops +at the first level large enough. + +The walk is one step of a loop, so `Cobham.iterate_mem_FP` runs it; the state is +the level so far and the size so far, carried beside the requested count. + +## Main definitions + +- `Complexity.levelStep` — one tick of the search + +## Main results + +- `Complexity.levelStep_iterate` — what the loop has found after `j` ticks +- `Complexity.levelStep_mem_FP` — the tick is polynomial time +- `Complexity.levelAfter_of_lt`, `Complexity.levelAfter_stable` — the loop + climbs until it is large enough, and then stays +- `Complexity.levelAfter_snd_le` — and never overshoots by more than a factor +- `Complexity.levelFn_mem_FP` — the search is polynomial time +- `Complexity.levelFn_length` — and finds the first level that is large enough +- `Complexity.pow_levelFn_le` — whatever level it reports, that level's size is + polynomially bounded +-/ + +@[expose] public section + +namespace Complexity + +/-- The loop's own model: after `j` ticks, the level and the size reached. -/ +def levelAfter (d n : ℕ) : ℕ → ℕ × ℕ → ℕ × ℕ + | 0, p => p + | j + 1, p => + let q := levelAfter d n j p + if q.2 < 2 * n then (q.1 + 1, q.2 * d) else q + +/-- One tick: if the size so far is below twice the request, take another +level. The state is `pair (pair (level so far) (size so far)) (the request)`. -/ +noncomputable def levelStep (d : ℕ) (st : List Bool) : List Bool := + ifLtLen (pairSnd (pairFst st)) + (pairSnd st ++ pairSnd st) + (pair (pair (pairFst (pairFst st) ++ [true]) + ((marks (mulC d (pairSnd (pairFst st)))).take + (List.replicate d true ++ mulC (2 * d) (pairSnd st)).length)) + (pairSnd st)) + st + +theorem levelStep_mem_FP (d : ℕ) : levelStep d ∈ FP := by + have hk : (fun st : List Bool => pairFst (pairFst st)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + have hs : (fun st : List Bool => pairSnd (pairFst st)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + have hn : (fun st : List Bool => pairSnd st) ∈ FP := Cobham.sndBlock_mem_FP + have hbound : (fun st : List Bool => + List.replicate d true ++ mulC (2 * d) (pairSnd st)) ∈ FP := + Cobham.appendFn_mem_FP (constFn_mem_FP (List.replicate d true)) (mulC_mem_FP hn (2 * d)) + have hadv : (fun st : List Bool => + pair (pair (pairFst (pairFst st) ++ [true]) + ((marks (mulC d (pairSnd (pairFst st)))).take + (List.replicate d true ++ mulC (2 * d) (pairSnd st)).length)) + (pairSnd st)) ∈ FP := + Cobham.pairFn_mem_FP + (Cobham.pairFn_mem_FP (Cobham.appendFn_mem_FP hk (constFn_mem_FP [true])) + (Cobham.takeLenFn_mem_FP hbound (marks_mem_FP (mulC_mem_FP hs d)))) hn + exact ifLtLen_mem_FP hs (Cobham.appendFn_mem_FP hn hn) hadv id_mem_FP + +theorem levelStep_apply (d k s : ℕ) (Z : List Bool) : + levelStep d (pair (pair (List.replicate k true) (List.replicate s true)) Z) + = if s < 2 * Z.length then + pair (pair (List.replicate (k + 1) true) (List.replicate (s * d) true)) Z + else pair (pair (List.replicate k true) (List.replicate s true)) Z := by + set n := Z.length with hn + rw [levelStep, pairFst_pair, pairSnd_pair, pairFst_pair, + pairSnd_pair] + have hlen : (Z ++ Z).length = 2 * n := by + simp only [List.length_append] + omega + by_cases h : s < 2 * n + · rw [if_pos h, ifLtLen_pos (by rw [List.length_replicate, hlen]; exact h), + marks_eq, length_mulC, List.length_replicate, ← List.replicate_succ'] + congr 2 + refine List.take_of_length_le ?_ + rw [List.length_replicate, List.length_append, List.length_replicate, length_mulC, ← hn] + nlinarith [h] + · rw [if_neg h, ifLtLen_neg (by rw [List.length_replicate, hlen]; exact h)] + +/-- **The loop's model runs the loop.** -/ +theorem levelStep_iterate (d : ℕ) (Z : List Bool) : + ∀ (j k s : ℕ), (levelStep d)^[j] + (pair (pair (List.replicate k true) (List.replicate s true)) Z) + = pair (pair (List.replicate (levelAfter d Z.length j (k, s)).1 true) + (List.replicate (levelAfter d Z.length j (k, s)).2 true)) Z := by + intro j + induction j with + | zero => intro k s; rfl + | succ j ih => + intro k s + rw [Function.iterate_succ_apply', ih k s, levelStep_apply, levelAfter] + by_cases h : (levelAfter d Z.length j (k, s)).2 < 2 * Z.length + · rw [if_pos h, if_pos h] + · rw [if_neg h, if_neg h] + +/-! ### What the loop settles on -/ + +/-- Until it is large enough, the loop is at level `j` with size `d ^ (j + 1)`. -/ +theorem levelAfter_of_lt (d n : ℕ) : + ∀ j, (∀ i < j, ¬ (2 * n ≤ d ^ (i + 1))) → levelAfter d n j (0, d) = (j, d ^ (j + 1)) := by + intro j + induction j with + | zero => intro _; simp [levelAfter] + | succ j ih => + intro h + have hj := ih fun i hi => h i (by omega) + rw [levelAfter, hj] + have hlt : d ^ (j + 1) < 2 * n := by + have := h j (by omega) + omega + rw [if_pos hlt] + refine Prod.ext rfl ?_ + show d ^ j * d * d = d ^ (j + 1 + 1) + rw [pow_succ, pow_succ] + +/-- Once it is large enough, the loop stays put. -/ +theorem levelAfter_stable (d n : ℕ) (p : ℕ × ℕ) (j : ℕ) (h : 2 * n ≤ (levelAfter d n j p).2) : + ∀ i, levelAfter d n (j + i) p = levelAfter d n j p := by + intro i + induction i with + | zero => rfl + | succ i ih => + have hji : j + (i + 1) = (j + i) + 1 := by omega + rw [hji, levelAfter, ih, if_neg (by omega)] + +/-- The loop never overshoots by more than a factor of `d`. -/ +theorem levelAfter_snd_le (d n : ℕ) : + ∀ j, (levelAfter d n j (0, d)).2 ≤ d + 2 * n * d := by + intro j + induction j with + | zero => simp [levelAfter] + | succ j ih => + rw [levelAfter] + by_cases h : (levelAfter d n j (0, d)).2 < 2 * n + · rw [if_pos h] + have : (levelAfter d n j (0, d)).2 * d ≤ 2 * n * d := Nat.mul_le_mul_right _ (by omega) + simpa using by omega + · rw [if_neg h] + exact ih + +/-- The size the loop carries is always the power the level names. -/ +theorem levelAfter_pow (d n : ℕ) : + ∀ j, (levelAfter d n j (0, d)).2 = d ^ ((levelAfter d n j (0, d)).1 + 1) := by + intro j + induction j with + | zero => simp [levelAfter] + | succ j ih => + rw [levelAfter] + by_cases h : (levelAfter d n j (0, d)).2 < 2 * n + · rw [if_pos h] + show (levelAfter d n j (0, d)).2 * d = d ^ ((levelAfter d n j (0, d)).1 + 1 + 1) + rw [ih] + ring + · rw [if_neg h] + exact ih + +/-- **The level the loop reaches names a size below `d + 2 n d`.** -/ +theorem pow_levelAfter_le (d n : ℕ) (j : ℕ) : + d ^ ((levelAfter d n j (0, d)).1 + 1) ≤ d + 2 * n * d := by + rw [← levelAfter_pow d n j] + exact levelAfter_snd_le d n j + +/-! ### The search as one function -/ + +/-- The shape of the state after `j` ticks: a level of at most `j` marks and a +size the clamp keeps below `d + 2 |z| d`, beside the request. -/ +theorem levelStep_iterate_shape (d : ℕ) (z : List Bool) : + ∀ j, ∃ K S : List Bool, + (levelStep d)^[j] (pair (pair [] (List.replicate d true)) z) = pair (pair K S) z + ∧ K.length ≤ j ∧ S.length ≤ d + 2 * z.length * d := by + intro j + induction j with + | zero => + refine ⟨[], List.replicate d true, rfl, by simp, ?_⟩ + rw [List.length_replicate] + omega + | succ j ih => + obtain ⟨K, S, hst, hK, hS⟩ := ih + rw [Function.iterate_succ_apply', hst, levelStep, pairFst_pair, + pairSnd_pair, pairFst_pair, pairSnd_pair] + by_cases h : S.length < (z ++ z).length + · refine ⟨K ++ [true], + (marks (mulC d S)).take (List.replicate d true ++ mulC (2 * d) z).length, ?_, ?_, ?_⟩ + · rw [ifLtLen_pos h] + · rw [List.length_append, List.length_cons, List.length_nil] + omega + · rw [List.length_take, List.length_append, List.length_replicate, length_mulC, + show d + z.length * (2 * d) = d + 2 * z.length * d from by ring] + exact Nat.min_le_left _ _ + · exact ⟨K, S, by rw [ifLtLen_neg h], by omega, hS⟩ + +theorem levelStep_iterate_length_le (d : ℕ) (z : List Bool) (j : ℕ) : + ((levelStep d)^[j] (pair (pair [] (List.replicate d true)) z)).length + ≤ 2 * (2 * j + 2 + (d + 2 * z.length * d)) + 2 + z.length := by + obtain ⟨K, S, hst, hK, hS⟩ := levelStep_iterate_shape d z j + rw [hst, pair_length, pair_length] + omega + +/-- The polynomial that bounds the loop's state. -/ +noncomputable def levelWidth (d : ℕ) (p : Polynomial ℕ) : Polynomial ℕ := + Polynomial.C 4 * p + Polynomial.C (2 * d + 6) + + Polynomial.C (4 * d + 1) * Polynomial.X + +/-- **The tower level for a requested count**, as one function: run the search +for polynomially many ticks and read off the level. -/ +noncomputable def levelFn (d : ℕ) (p : Polynomial ℕ) (z : List Bool) : List Bool := + pairFst (pairFst + ((levelStep d)^[(polyRuler p z).length] (pair (pair [] (List.replicate d true)) z))) + +theorem levelFn_mem_FP (d : ℕ) (p : Polynomial ℕ) : levelFn d p ∈ FP := by + have hinit : (fun z : List Bool => pair (pair [] (List.replicate d true)) z) ∈ FP := + Cobham.pairFn_mem_FP (constFn_mem_FP (pair [] (List.replicate d true))) id_mem_FP + have hruler : (fun z : List Bool => polyRuler p z) ∈ FP := polyRulerFn_mem_FP p id_mem_FP + have hwidth : (fun z : List Bool => polyRuler (levelWidth d p) z) ∈ FP := + polyRulerFn_mem_FP _ id_mem_FP + have hbound : ∀ z : List Bool, ∀ j ≤ (polyRuler p z).length, + ((levelStep d)^[j] (pair (pair [] (List.replicate d true)) z)).length + ≤ (polyRuler (levelWidth d p) z).length := by + intro z j hj + rw [polyRuler_length] at hj ⊢ + refine le_trans (levelStep_iterate_length_le d z j) ?_ + have heval : (levelWidth d p).eval z.length + = 4 * p.eval z.length + (2 * d + 6) + (4 * d + 1) * z.length := by + simp only [levelWidth, Polynomial.eval_add, Polynomial.eval_mul, Polynomial.eval_C, + Polynomial.eval_X] + rw [heval] + nlinarith [hj] + have hiter := Cobham.iterate_mem_FP (levelStep_mem_FP d) hinit hruler hwidth hbound + have hproj := mem_FP_comp (mem_FP_comp hiter Cobham.fstBlock_mem_FP) Cobham.fstBlock_mem_FP + exact mem_FP_of_eq hproj fun z => rfl + +/-- The size at the level the search reports. -/ +noncomputable def sizeFn (d : ℕ) (p : Polynomial ℕ) (z : List Bool) : List Bool := + pairSnd (pairFst + ((levelStep d)^[(polyRuler p z).length] (pair (pair [] (List.replicate d true)) z))) + +theorem sizeFn_mem_FP (d : ℕ) (p : Polynomial ℕ) : sizeFn d p ∈ FP := by + have hinit : (fun z : List Bool => pair (pair [] (List.replicate d true)) z) ∈ FP := + Cobham.pairFn_mem_FP (constFn_mem_FP (pair [] (List.replicate d true))) id_mem_FP + have hruler : (fun z : List Bool => polyRuler p z) ∈ FP := polyRulerFn_mem_FP p id_mem_FP + have hwidth : (fun z : List Bool => polyRuler (levelWidth d p) z) ∈ FP := + polyRulerFn_mem_FP _ id_mem_FP + have hbound : ∀ z : List Bool, ∀ j ≤ (polyRuler p z).length, + ((levelStep d)^[j] (pair (pair [] (List.replicate d true)) z)).length + ≤ (polyRuler (levelWidth d p) z).length := by + intro z j hj + rw [polyRuler_length] at hj ⊢ + refine le_trans (levelStep_iterate_length_le d z j) ?_ + have heval : (levelWidth d p).eval z.length + = 4 * p.eval z.length + (2 * d + 6) + (4 * d + 1) * z.length := by + simp only [levelWidth, Polynomial.eval_add, Polynomial.eval_mul, Polynomial.eval_C, + Polynomial.eval_X] + rw [heval] + nlinarith [hj] + have hiter := Cobham.iterate_mem_FP (levelStep_mem_FP d) hinit hruler hwidth hbound + have hproj := mem_FP_comp (mem_FP_comp hiter Cobham.fstBlock_mem_FP) Cobham.sndBlock_mem_FP + exact mem_FP_of_eq hproj fun z => rfl + +/-- **The size the search reports is the power its level names.** -/ +theorem sizeFn_length (d : ℕ) (p : Polynomial ℕ) (z : List Bool) : + (sizeFn d p z).length = d ^ ((levelFn d p z).length + 1) := by + have hinit : (pair ([] : List Bool) (List.replicate d true)) + = pair (List.replicate 0 true) (List.replicate d true) := rfl + rw [sizeFn, levelFn, hinit, levelStep_iterate d z _ 0 d, pairFst_pair, + pairFst_pair, pairSnd_pair, List.length_replicate, List.length_replicate] + exact levelAfter_pow d z.length _ + +/-- **The search finds the first level that is large enough.** -/ +theorem levelFn_length (d : ℕ) (p : Polynomial ℕ) (z : List Bool) (L : ℕ) + (hL : 2 * z.length ≤ d ^ (L + 1)) (hmin : ∀ i < L, ¬ (2 * z.length ≤ d ^ (i + 1))) + (hp : L ≤ p.eval z.length) : + (levelFn d p z).length = L := by + have hinit : (pair ([] : List Bool) (List.replicate d true)) + = pair (List.replicate 0 true) (List.replicate d true) := rfl + have hL' : levelAfter d z.length L (0, d) = (L, d ^ (L + 1)) := levelAfter_of_lt d z.length L hmin + have hstable : ∀ i, levelAfter d z.length (L + i) (0, d) = (L, d ^ (L + 1)) := by + intro i + rw [levelAfter_stable d z.length (0, d) L (by rw [hL']; exact hL) i, hL'] + obtain ⟨i, hi⟩ : ∃ i, p.eval z.length = L + i := ⟨p.eval z.length - L, by omega⟩ + rw [levelFn, hinit, polyRuler_length, hi, levelStep_iterate d z (L + i) 0 d, hstable i, + pairFst_pair, pairFst_pair, List.length_replicate] + +/-- **Whatever level the search reports, its size is bounded** — which is what +lets the table at that level be written down. -/ +theorem pow_levelFn_le (d : ℕ) (p : Polynomial ℕ) (z : List Bool) : + d ^ ((levelFn d p z).length + 1) ≤ d + 2 * z.length * d := by + have hinit : (pair ([] : List Bool) (List.replicate d true)) + = pair (List.replicate 0 true) (List.replicate d true) := rfl + have hlen : (levelFn d p z).length + = (levelAfter d z.length (polyRuler p z).length (0, d)).1 := by + rw [levelFn, hinit, levelStep_iterate d z _ 0 d, pairFst_pair, + pairFst_pair, List.length_replicate] + rw [hlen] + exact pow_levelAfter_le d z.length _ + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgLog.lean b/Complexitylib/Classes/PCP/Internal/AlgLog.lean new file mode 100644 index 00000000..374ba729 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgLog.lean @@ -0,0 +1,235 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.HeadOps +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.PCP.Internal.Materialize + +/-! +# A ruler of logarithmic length + +Amplification runs logarithmically many rounds, and the bounded-iteration rule +counts rounds by the *length* of a string. So an algorithm needs to write a +string whose length is the logarithm of its input's — the one shrinking step the +rest of the development never needed. + +The construction folds over the input carrying two counters: a ruler, and a +threshold that doubles. Every time the number of bits read reaches the +threshold, the ruler gains a mark and the threshold doubles, so the ruler counts +the doublings. + +## Main definitions + +- `Complexity.logStep` — one step of that fold +- `Complexity.logRuler` — the ruler itself + +## Main results + +- `Complexity.logRuler_mem_FP` — it is an `FP` function +-/ + +@[expose] public section + +namespace Complexity + +/-- How long the ruler is after reading `n` bits: it gains a mark exactly when +the count reaches the next power of two. -/ +def rulerLen : ℕ → ℕ + | 0 => 0 + | n + 1 => if n + 1 < 2 ^ rulerLen n then rulerLen n else rulerLen n + 1 + +/-- One step of the ruler fold, on `pair (pair W acc) t` where `acc` is +`pair ruler threshold`: on reaching the threshold, add a mark and double. -/ +noncomputable def logStep (z : List Bool) : List Bool := + ifLtLen (pairSnd z) (dropOne (pairSnd (pairSnd + (pairFst z)))) + (pairSnd (pairFst z)) + (pair (pairFst (pairSnd (pairFst z)) ++ [true]) + (pairSnd (pairSnd (pairFst z)) + ++ pairSnd (pairSnd (pairFst z)))) + +theorem logStep_mem_FP : logStep ∈ FP := by + have hacc : (fun z : List Bool => pairSnd (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + have hr : (fun z : List Bool => + pairFst (pairSnd (pairFst z))) ∈ FP := + mem_FP_comp hacc Cobham.fstBlock_mem_FP + have hth : (fun z : List Bool => + pairSnd (pairSnd (pairFst z))) ∈ FP := + mem_FP_comp hacc Cobham.sndBlock_mem_FP + exact ifLtLen_mem_FP Cobham.sndBlock_mem_FP (dropOneFn_mem_FP hth) hacc + (Cobham.pairFn_mem_FP (Cobham.appendFn_mem_FP hr (constFn_mem_FP [true])) + (Cobham.appendFn_mem_FP hth hth)) + +theorem rulerLen_le (n : ℕ) : rulerLen n ≤ n := by + induction n with + | zero => rfl + | succ n ih => + rw [rulerLen] + split <;> omega + +theorem two_pow_rulerLen_le (n : ℕ) : 2 ^ rulerLen n ≤ 2 * n + 1 := by + induction n with + | zero => simp [rulerLen] + | succ n ih => + rw [rulerLen] + by_cases h : n + 1 < 2 ^ rulerLen n + · rw [if_pos h] + omega + · rw [if_neg h, pow_succ] + omega + +theorem lt_two_pow_rulerLen (n : ℕ) : n < 2 ^ rulerLen n := by + induction n with + | zero => simp [rulerLen] + | succ n ih => + rw [rulerLen] + by_cases h : n + 1 < 2 ^ rulerLen n + · rw [if_pos h] + exact h + · rw [if_neg h, pow_succ] + omega + +theorem rulerLen_pos {n : ℕ} (hn : 0 < n) : 0 < rulerLen n := by + cases n with + | zero => omega + | succ n => + rw [rulerLen] + split + · rename_i h + by_contra hc + have h0 : rulerLen n = 0 := by omega + rw [h0] at h + simp at h + · omega + +/-- The ruler is at most a logarithm: `2 ^ rulerLen n ≤ 2n + 1`. -/ +theorem rulerLen_le_log (n : ℕ) : rulerLen n ≤ Nat.log 2 (2 * n + 1) := by + refine le_trans (le_of_eq (Nat.log_pow (b := 2) (by omega) (rulerLen n)).symm) ?_ + exact Nat.log_mono_right (two_pow_rulerLen_le n) + +/-- **A ruler over a polynomially bounded quantity is logarithmic.** -/ +theorem rulerLen_bigO_log {N : ℕ → ℕ} {A B : ℕ} (hN : ∀ n, N n ≤ A * (n + 1) ^ B) : + (fun n => rulerLen (N n)) =O (fun n => Nat.log 2 n) := by + refine Asymptotics.IsBigO.of_bound (Nat.log 2 A + 2 * B + 6) ?_ + filter_upwards [Filter.eventually_ge_atTop 2] with n hn + simp only [Real.norm_natCast] + have hlog : 1 ≤ Nat.log 2 n := Nat.log_pos (by omega) hn + have hA : A ≤ 2 ^ (Nat.log 2 A + 1) := le_of_lt (Nat.lt_pow_succ_log_self (by omega) A) + have hn1 : n + 1 ≤ 2 ^ (Nat.log 2 n + 1) := by + have := Nat.lt_pow_succ_log_self (b := 2) (by omega) n + omega + have hpow : (n + 1) ^ B ≤ 2 ^ ((Nat.log 2 n + 1) * B) := by + rw [pow_mul] + exact Nat.pow_le_pow_left hn1 B + have hbound : 2 * N n + 1 ≤ 2 ^ (Nat.log 2 A + 3 + (Nat.log 2 n + 1) * B) := by + have h1 : N n ≤ 2 ^ (Nat.log 2 A + 1) * 2 ^ ((Nat.log 2 n + 1) * B) := + le_trans (hN n) (Nat.mul_le_mul hA hpow) + have h2 : 2 ^ (Nat.log 2 A + 3 + (Nat.log 2 n + 1) * B) + = 4 * (2 ^ (Nat.log 2 A + 1) * 2 ^ ((Nat.log 2 n + 1) * B)) := by + rw [← pow_add, show (4 : ℕ) = 2 ^ 2 from rfl, ← pow_add] + congr 1 + ring + have h3 : 0 < 2 ^ (Nat.log 2 A + 1) * 2 ^ ((Nat.log 2 n + 1) * B) := + Nat.mul_pos (Nat.two_pow_pos _) (Nat.two_pow_pos _) + omega + have hle : rulerLen (N n) ≤ Nat.log 2 A + 3 + (Nat.log 2 n + 1) * B := by + refine le_trans (rulerLen_le_log (N n)) ?_ + refine le_trans (Nat.log_mono_right hbound) ?_ + rw [Nat.log_pow (by omega)] + have hfin : rulerLen (N n) ≤ (Nat.log 2 A + 2 * B + 6) * Nat.log 2 n := by + have hmul : (Nat.log 2 n + 1) * B ≤ 2 * B * Nat.log 2 n := by + have : Nat.log 2 n + 1 ≤ 2 * Nat.log 2 n := by omega + calc (Nat.log 2 n + 1) * B ≤ (2 * Nat.log 2 n) * B := Nat.mul_le_mul_right _ this + _ = 2 * B * Nat.log 2 n := by ring + have hconst : Nat.log 2 A + 3 ≤ (Nat.log 2 A + 6) * Nat.log 2 n := by + calc Nat.log 2 A + 3 ≤ (Nat.log 2 A + 6) * 1 := by omega + _ ≤ (Nat.log 2 A + 6) * Nat.log 2 n := Nat.mul_le_mul_left _ hlog + calc rulerLen (N n) ≤ Nat.log 2 A + 3 + (Nat.log 2 n + 1) * B := hle + _ ≤ (Nat.log 2 A + 6) * Nat.log 2 n + 2 * B * Nat.log 2 n := by omega + _ = (Nat.log 2 A + 2 * B + 6) * Nat.log 2 n := by ring + exact_mod_cast hfin + +/-- **The fold's value after reading a list.** -/ +theorem logFold_eq (bound : ℕ) : ∀ z : List Bool, 4 * z.length + 4 ≤ bound → + Cobham.recFoldClamp logStep logStep bound (pair [] [true]) [] z + = pair (List.replicate (rulerLen z.length) true) + (List.replicate (2 ^ rulerLen z.length) true) := by + intro z + induction z with + | nil => + intro _ + rw [Cobham.recFoldClamp] + show (pair [] [true]).take bound = pair (List.replicate (rulerLen 0) true) + (List.replicate (2 ^ rulerLen 0) true) + rw [show rulerLen 0 = 0 from rfl] + simp only [pow_zero, List.replicate_zero, List.replicate_one] + rw [List.take_of_length_le] + simp + omega + | cons b t ih => + intro hb + have hbt : 4 * t.length + 4 ≤ bound := by simp at hb ⊢; omega + rw [Cobham.recFoldClamp] + simp only [Bool.cond_self] + rw [ih hbt, logStep] + simp only [pairFst_pair, pairSnd_pair, dropOne] + have hb' : 4 * t.length + 8 ≤ bound := by simp at hb; omega + by_cases h : t.length < 2 ^ rulerLen t.length - 1 + · rw [ifLtLen_pos (by simpa using h), List.length_cons, + show rulerLen (t.length + 1) = rulerLen t.length from by + rw [rulerLen] + exact if_pos (by omega)] + refine List.take_of_length_le ?_ + rw [pair_length, List.length_replicate, List.length_replicate] + have h1 := rulerLen_le t.length + have h2 := two_pow_rulerLen_le t.length + omega + · rw [ifLtLen_neg (by simpa using h), List.length_cons, + show rulerLen (t.length + 1) = rulerLen t.length + 1 from by + rw [rulerLen] + exact if_neg (by omega), + ← List.replicate_add, ← two_mul, ← pow_succ', ← List.replicate_succ'] + refine List.take_of_length_le ?_ + rw [pair_length, List.length_replicate, List.length_replicate] + have h1 := rulerLen_le (t.length + 1) + have h2 := two_pow_rulerLen_le (t.length + 1) + rw [show rulerLen (t.length + 1) = rulerLen t.length + 1 from by + rw [rulerLen] + exact if_neg (by omega)] at h1 h2 + omega + +/-- The fold itself, on `pair W z`. -/ +noncomputable def logRulerRaw (w : List Bool) : List Bool := + Cobham.recFoldClamp logStep logStep (4 * w.length + 4) (pair [] [true]) + (pairFst w) (pairSnd w) + +theorem logRulerRaw_mem_FP : logRulerRaw ∈ FP := by + refine mem_FP_of_eq (Cobham.recFoldClamp_mem_FP logStep_mem_FP logStep_mem_FP + (constFn_mem_FP (pair [] [true])) (4 * Polynomial.X + 4)) fun w => ?_ + rw [logRulerRaw] + simp + +/-- **A ruler of logarithmic length.** -/ +noncomputable def logRuler (z : List Bool) : List Bool := + pairFst (logRulerRaw (pair [] z)) + +theorem logRuler_mem_FP : logRuler ∈ FP := + mem_FP_of_eq (mem_FP_comp (Cobham.pairFn_mem_FP (constFn_mem_FP []) id_mem_FP) + (mem_FP_comp logRulerRaw_mem_FP Cobham.fstBlock_mem_FP)) fun _ => rfl + +/-- **The ruler is as long as the fold says.** -/ +theorem logRuler_eq (z : List Bool) : + logRuler z = List.replicate (rulerLen z.length) true := by + rw [logRuler, logRulerRaw, pairFst_pair, pairSnd_pair, + logFold_eq _ z (by rw [pair_length]; simp), pairFst_pair] + +@[simp] theorem length_logRuler (z : List Bool) : + (logRuler z).length = rulerLen z.length := by + rw [logRuler_eq, List.length_replicate] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgPCP.lean b/Complexitylib/Classes/PCP/Internal/AlgPCP.lean new file mode 100644 index 00000000..4ee548f3 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgPCP.lean @@ -0,0 +1,209 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Asymptotics.PolyBound +public import Complexitylib.Classes.PCP.Internal.AlgGapCSP +public import Complexitylib.Classes.PCP.Internal.SquareVerifier + +/-! +# Every NP language has a PCP verifier + +The pieces are all in place: an `NP` language is the satisfiability of an `FP` +family of 3-CNFs, the gap reduction turns each into a constraint graph with a +constant gap, that graph is written by an `FP` function, and a verifier reading +one edge of it accepts members always and non-members with probability bounded +away from one. + +## Main definitions + +- `Complexity.gapNumEdges` — how many edges the gap graph of a length has + +## Main results + +- `Complexity.exists_pcp_of_mem_NP` — the hard half of the PCP theorem +-/ + +@[expose] public section + +set_option maxRecDepth 8000 + +namespace Complexity + +open SAT Dinur + +/-- The finite base the reduction's expander family comes from. -/ +noncomputable def algF : FinBase := algBase + +theorem algHd : 1 < algF.deg := one_lt_algBase_deg + +/-- How many edges the gap graph of an input of length `n` has: the padded +count, multiplied by the round's factor once per round. -/ +noncomputable def gapNumEdges (q : Polynomial ℕ) (n : ℕ) : ℕ := + edgeFactor (algF.toFamily algHd) (qOf algF algHd) ^ rulerLen (q.eval n) * q.eval n + +theorem gapNumEdges_pos {q : Polynomial ℕ} {n : ℕ} (hq : 0 < q.eval n) : + 0 < gapNumEdges q n := + Nat.mul_pos (Nat.pow_pos (one_le_edgeFactor algF algHd)) hq + +/-- The gap graph's size is polynomial in the input's length. -/ +theorem gapNumEdges_polyBound (q : Polynomial ℕ) : PolyBound (gapNumEdges q) := by + have hq : PolyBound fun n => q.eval n := PolyBound.eval q + refine PolyBound.mono (PolyBound.mul + (PolyBound.pow (PolyBound.add (PolyBound.mul (PolyBound.const 2) hq) + (PolyBound.const 1)) (growthExp algF algHd)) hq) fun n => ?_ + exact Nat.mul_le_mul_right _ (pow_edgeFactor_le algF algHd (q.eval n)) + +/-- **The coin count**: enough for the gap graph's edges, and no more than one +too many. -/ +noncomputable def gapCoins (q : Polynomial ℕ) (n : ℕ) : ℕ := rulerLen (gapNumEdges q n) + +theorem le_two_pow_gapCoins (q : Polynomial ℕ) (n : ℕ) : + gapNumEdges q n ≤ 2 ^ gapCoins q n := + le_of_lt (lt_two_pow_rulerLen _) + +theorem two_pow_gapCoins_le {q : Polynomial ℕ} {n : ℕ} (hq : 0 < q.eval n) : + 2 ^ gapCoins q n ≤ 2 * gapNumEdges q n := by + have hpos := gapNumEdges_pos (q := q) (n := n) hq + have hle := two_pow_rulerLen_le (gapNumEdges q n) + have hone : 0 < rulerLen (gapNumEdges q n) := rulerLen_pos hpos + have heven : 2 ∣ 2 ^ gapCoins q n := dvd_pow_self 2 (by rw [gapCoins]; omega) + obtain ⟨c, hc⟩ := heven + have hgc : gapCoins q n = rulerLen (gapNumEdges q n) := rfl + rw [hgc] at hc + rw [hgc] + omega + +theorem gapCoins_bigO_log (q : Polynomial ℕ) : + gapCoins q =O fun n => Nat.log 2 n := by + obtain ⟨A, B, hAB⟩ := (gapNumEdges_polyBound q).exists_mul_pow_bound + exact rulerLen_bigO_log hAB + +/-- Doubling a constructible bound `j` times keeps it constructible. -/ +theorem constructible_pow_mul {t : ℕ → ℕ} + (ht : (fun x : List Bool => List.replicate (t x.length) true) ∈ FP) (j : ℕ) : + (fun x : List Bool => List.replicate (2 ^ j * t x.length) true) ∈ FP := by + induction j with + | zero => simpa using ht + | succ j ih => + refine mem_FP_of_eq (constructible_double (r := fun n => 2 ^ j * t n) ih) fun x => ?_ + congr 1 + ring + +/-! ### The edge count of the algorithmic graph -/ + +theorem numEdges_gapAlg_eq {E padU : List Bool → List Bool} {Φ : List Bool → CNF} + (hgap : gapAll algF algHd E padU ∈ FP) + (hE : ∀ x, E x = (Φ x).encode) (h3 : ∀ x, CNF.Is3CNF (Φ x)) + (hmark : ∀ x, padU x = List.replicate (padU x).length true) + (hle : ∀ x, 3 * (Φ x).length ≤ (padU x).length) {q : Polynomial ℕ} + (hq : ∀ x, (padU x).length = q.eval x.length) (x : List Bool) : + (gapAlg algF algHd E padU hgap).numEdges x = gapNumEdges q x.length := by + rw [numEdges_gapAlg, gapAll_eq algF algHd E padU hE h3 hmark hle x, gEdges_encGraph, + gapAllG, numEdges_iterStep, ConstraintGraph.numEdges_padGraph, numEdges_baseCSP, + max_eq_left (by have := hle x; omega), hq, gapNumEdges] + +/-! ### The verifier -/ + +open scoped Complexity in +/-- **Every `NP` language has a PCP verifier** with logarithmically many coins +and constantly many queries. -/ +theorem exists_pcp_of_mem_NP {L : Language} (hL : L ∈ NP) : + ∃ r : ℕ → ℕ, r =O (fun n => Nat.log 2 n) ∧ Constructible r + ∧ ∃ qc : ℕ → ℕ, qc =O (fun _ => 1) ∧ L ∈ PCP r qc := by + classical + obtain ⟨E, Φ, hEfp, hEeq, h3, hLiff⟩ := exists_reduction_cnf hL + obtain ⟨pad0, q0, hpad0fp, hmark0, hq0, hle0⟩ := exists_padRuler hEfp 3 + set padU : List Bool → List Bool := fun x => pad0 x ++ [true] with hpadU + set q : Polynomial ℕ := q0 + 1 with hqdef + have hpadfp : padU ∈ FP := Cobham.appendFn_mem_FP hpad0fp (constFn_mem_FP [true]) + have hlen : ∀ x, (padU x).length = (pad0 x).length + 1 := by + intro x + rw [hpadU] + simp + have hmark : ∀ x, padU x = List.replicate (padU x).length true := by + intro x + rw [hlen x, List.replicate_succ', ← hmark0 x, hpadU] + have hq : ∀ x : List Bool, (padU x).length = q.eval x.length := by + intro x + rw [hlen x, hq0 x, hqdef, Polynomial.eval_add, Polynomial.eval_one] + have hqpos : ∀ n, 0 < q.eval n := by + intro n + rw [hqdef, Polynomial.eval_add, Polynomial.eval_one] + omega + have hle : ∀ x, 3 * (Φ x).length ≤ (padU x).length := by + intro x + have h1 := hle0 x + have h2 := length_le_length_encode (Φ x) + rw [← hEeq x] at h2 + rw [hlen x] + omega + obtain ⟨p0, hp0⟩ := exists_length_bound hEfp + have hgap : gapAll algF algHd E padU ∈ FP := + gapAll_mem_FP algF algHd E padU hEfp hpadfp hEeq h3 hmark hle p0 q hp0 hq + have hmodels := gapAlg_models algF algHd E padU hgap hEeq h3 hmark hle + have hNE : ∀ x, (gapAlg algF algHd E padU hgap).numEdges x = gapNumEdges q x.length := + fun x => numEdges_gapAlg_eq hgap hEeq h3 hmark hle hq x + have ht : (fun x : List Bool => List.replicate (gapCoins q x.length) true) ∈ FP := by + have hfp : (fun x : List Bool => + logRuler (posCount (pairSnd (gapAll algF algHd E padU x)))) ∈ FP := + mem_FP_of_eq (mem_FP_comp (gEdgesFn_mem_FP hgap) logRuler_mem_FP) fun _ => rfl + refine mem_FP_of_eq hfp fun x => ?_ + have h : gEdges (gapAll algF algHd E padU x) = gapNumEdges q x.length := by + rw [← numEdges_gapAlg algF algHd E padU hgap x] + exact hNE x + rw [logRuler_eq, length_posCount_sndBlock, h, gapCoins] + obtain ⟨Ac, Bc, hABc⟩ := (gapNumEdges_polyBound q).exists_mul_pow_bound + have hclamp : ∀ n : ℕ, 2 ^ gapCoins q n + ≤ (Polynomial.C (2 * Ac + 1) * (Polynomial.X + 1) ^ Bc).eval + (2 * n + 2 + gapCoins q n) := by + intro n + have h1 : 2 ^ gapCoins q n ≤ 2 * gapNumEdges q n := two_pow_gapCoins_le (hqpos n) + have h2 : gapNumEdges q n ≤ Ac * (n + 1) ^ Bc := hABc n + have h4 : (n + 1) ^ Bc ≤ (2 * n + 2 + gapCoins q n + 1) ^ Bc := + Nat.pow_le_pow_left (by omega) _ + have h5 : (Polynomial.C (2 * Ac + 1) * (Polynomial.X + 1) ^ Bc).eval + (2 * n + 2 + gapCoins q n) + = (2 * Ac + 1) * (2 * n + 2 + gapCoins q n + 1) ^ Bc := by + rw [Polynomial.eval_mul, Polynomial.eval_C, Polynomial.eval_pow, + Polynomial.eval_add, Polynomial.eval_X, Polynomial.eval_one] + have h6 : 2 * (Ac * (n + 1) ^ Bc) + ≤ (2 * Ac + 1) * (2 * n + 2 + gapCoins q n + 1) ^ Bc := by + calc 2 * (Ac * (n + 1) ^ Bc) = (2 * Ac) * (n + 1) ^ Bc := by ring + _ ≤ (2 * Ac + 1) * (2 * n + 2 + gapCoins q n + 1) ^ Bc := + Nat.mul_le_mul (by omega) h4 + rw [h5] + omega + have hcomp : ∀ x ∈ L, ∃ π : List Bool, + ∀ e < (gapAlg algF algHd E padU hgap).numEdges x, + (gapAlg algF algHd E padU hgap).Sat x π e := by + intro x hx + exact hmodels.sat_of_satisfiable x + (satisfiable_gapAllG algF algHd padU h3 hle x ((hLiff x).mp hx)) + have hsound : ∀ x ∉ L, ∀ π : List Bool, + (((Finset.range ((gapAlg algF algHd E padU hgap).numEdges x)).filter + ((gapAlg algF algHd E padU hgap).Sat x π)).card : ℚ) + ≤ (1 - (Dinur.amplifier (algF.toFamily algHd)).gap) + * (gapAlg algF algHd E padU hgap).numEdges x := by + intro x hx π + exact hmodels.card_sat_le x + (gap_le_unsatVal_gapAllG algF algHd padU h3 hle x + (fun hs => hx ((hLiff x).mpr hs))) π + obtain ⟨j, hj⟩ := mem_PCP_of_algCSP (gapAlg algF algHd E padU hgap) + (Polynomial.C (2 * Ac + 1) * (Polynomial.X + 1) ^ Bc) (gapCoins q) ht hclamp + (fun x => by rw [hNE x]; exact le_two_pow_gapCoins q x.length) + (fun x => by rw [hNE x]; exact two_pow_gapCoins_le (hqpos x.length)) + (Dinur.amplifier (algF.toFamily algHd)).gap_pos + (Dinur.amplifier (algF.toFamily algHd)).gap_le_one hcomp hsound + refine ⟨fun n => 2 ^ j * gapCoins q n, ?_, ?_, + fun _ => 2 ^ j * (2 * (gapAlg algF algHd E padU hgap).width), ?_, hj⟩ + · exact BigO.const_mul_left _ (gapCoins_bigO_log q) + · exact constructible_pow_mul ht j + · have h := BigO.const_mul_left (2 ^ j * (2 * (gapAlg algF algHd E padU hgap).width)) + (BigO.refl fun _ : ℕ => 1) + simpa using h + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgPosNum.lean b/Complexitylib/Classes/PCP/Internal/AlgPosNum.lean new file mode 100644 index 00000000..fd9d5c4d --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgPosNum.lean @@ -0,0 +1,149 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.AlgCompose +public import Complexitylib.Classes.PCP.Internal.AlgPreRel +public import Complexitylib.Classes.PCP.Internal.AlgStep + +/-! +# A composed position's number + +A read of the assembled tester lands in one of three kinds of block: the +encoding block of a vertex, a dart's linear table, or a dart's quadratic table. +Which kind, and which cube inside the block, depends only on the read and the +random string — with two exceptions, where the cube is shifted by the +arithmetization of the dart's satisfying set. Which *block*, on the other hand, +is a vertex or a dart of the outer graph, so it is the only part that grows with +the input. + +This module splits a position's number along that seam. + +## Main definitions + +- `Complexity.RegCSP.readKind` — which kind of block a read lands in +- `Complexity.RegCSP.blockNum`, `Complexity.RegCSP.cubeNum` — the block and the + cube inside it +- `Complexity.RegCSP.posNum` — the number the two make + +## Main results + +- `Complexity.RegCSP.enc_pos_compose` — that number is the position's +-/ + +@[expose] public section + +namespace Complexity + +open BooleanAnalysis Tester + +namespace RegCSP + +variable {β : Type} [Fintype β] [DecidableEq β] [Nonempty β] (R : RegCSP β) + [NumEnc R.graph.V] [NumEnc R.graph.D] {B : ℕ} (enc : β → Cube B) + +/-- Which kind of block a read's position lies in: an encoding block (`0`), a +dart's linear table (`1`), or a dart's quadratic table (`2`). -/ +def readKind : ReadIdx → ℕ + | .i5r | .i6r => 0 + | .g2x | .g2y | .g2s | .c3cQ | .c3tQ | .k4qG | .k4tG => 2 + | _ => 1 + +/-- The number of the vertex or dart whose block a read's position lies in. -/ +noncomputable def blockNum (p : R.Dart) : ReadIdx → ℕ + | .i5r => NumEnc.enc p.1 + | .i6r => NumEnc.enc (R.graph.nbr p.1 p.2) + | .f1x | .f1y | .f1s | .g2x | .g2y | .g2s | .c3cQ | .c3tQ | .c3cX | .c3xX | .c3cY | .c3yY + | .k4qG | .k4tG | .k4cF | .k4lF | .i5c | .i5b | .i6c | .i6b => NumEnc.enc p + +/-- The cube a read names, from the satisfying set alone. -/ +noncomputable def cubeOfSet (S : Finset (Cube (kOf B))) (z : Cube (ROf B)) : ReadIdx → ℕ + | .f1x => NumEnc.enc (leftBlock (blk1 z)) + | .f1y => NumEnc.enc (rightBlock (blk1 z)) + | .f1s => NumEnc.enc (leftBlock (blk1 z) + rightBlock (blk1 z)) + | .g2x => NumEnc.enc (leftBlock (blk2 z)) + | .g2y => NumEnc.enc (rightBlock (blk2 z)) + | .g2s => NumEnc.enc (leftBlock (blk2 z) + rightBlock (blk2 z)) + | .c3cQ => NumEnc.enc (cQ (blk3 z)) + | .c3tQ => NumEnc.enc (tensor (qX (blk3 z)) (qY (blk3 z)) + cQ (blk3 z)) + | .c3cX => NumEnc.enc (cX (blk3 z)) + | .c3xX => NumEnc.enc (qX (blk3 z) + cX (blk3 z)) + | .c3cY => NumEnc.enc (cY (blk3 z)) + | .c3yY => NumEnc.enc (qY (blk3 z) + cY (blk3 z)) + | .k4qG => NumEnc.enc (rightBlock (rightBlock (blk4 z))) + | .k4tG => NumEnc.enc ((QuadConstraint.combine (oneHotSystem S) + (leftBlock (blk4 z))).quad + rightBlock (rightBlock (blk4 z))) + | .k4cF => NumEnc.enc (leftBlock (rightBlock (blk4 z))) + | .k4lF => NumEnc.enc ((QuadConstraint.combine (oneHotSystem S) + (leftBlock (blk4 z))).lin + leftBlock (rightBlock (blk4 z))) + | .i5r => NumEnc.enc (leftBlock (blk5 z)) + | .i5c => NumEnc.enc (rightBlock (blk5 z)) + | .i5b => NumEnc.enc (basisVec (inTail B (leftBlock (blk5 z))) + rightBlock (blk5 z)) + | .i6r => NumEnc.enc (leftBlock (blk6 z)) + | .i6c => NumEnc.enc (rightBlock (blk6 z)) + | .i6b => NumEnc.enc (basisVec (inHead B (leftBlock (blk6 z))) + rightBlock (blk6 z)) + +/-- The number of the cube a read's position names inside its block. -/ +noncomputable def cubeNum (p : R.Dart) (z : Cube (ROf B)) : ReadIdx → ℕ := + cubeOfSet (R.satSet enc p) z + +/-- The number a kind, a block and a cube make: encoding blocks first, then the +linear tables, then the quadratic ones. -/ +def posNum (cardV cardD cardB cardN cardNN k w c : ℕ) : ℕ := + if k = 0 then w * cardB + c + else if k = 1 then cardV * cardB + (w * cardN + c) + else cardV * cardB + (cardV * cardD * cardN + (w * cardNN + c)) + +omit [DecidableEq β] [Nonempty β] in +/-- **A composed position's number.** -/ +theorem enc_pos_compose (p : R.Dart) (z : Cube (ROf B)) (i : ReadIdx) : + NumEnc.enc ((R.compose enc).pos p z i) + = posNum (NumEnc.card R.graph.V) (NumEnc.card R.graph.D) (NumEnc.card (Cube B)) + (NumEnc.card (Cube (nOf B))) (NumEnc.card (Cube (nOf B * nOf B))) + (readKind i) (R.blockNum p i) (R.cubeNum enc p z i) := by + cases i <;> rfl + +omit [DecidableEq β] [Nonempty β] in +/-- The test's verdict, from the satisfying set alone. -/ +noncomputable def checkOfSet (S : Finset (Cube (kOf B))) (z : Cube (ROf B)) + (rd : ReadIdx → ZMod 2) : Bool := + decide (bitFormula S z rd) + +omit [DecidableEq β] [Nonempty β] [NumEnc R.graph.V] [NumEnc R.graph.D] in +/-- **The cube depends on the satisfying set alone.** -/ +theorem cubeNum_eq_cubeOfSet (p : R.Dart) (z : Cube (ROf B)) (i : ReadIdx) : + R.cubeNum enc p z i = cubeOfSet (R.satSet enc p) z i := rfl + +omit [DecidableEq β] [Nonempty β] [NumEnc R.graph.V] [NumEnc R.graph.D] in +/-- **And so does the verdict.** -/ +theorem check_eq_checkOfSet (p : R.Dart) (z : Cube (ROf B)) : + (R.compose enc).check p z = checkOfSet (R.satSet enc p) z := rfl + +omit [DecidableEq β] [Nonempty β] in +set_option maxHeartbeats 800000 in +/-- **An edge's data and all three of its numbers**, in one package: a caller +never has to spell the composed system out, nor match anything against it. -/ +theorem edge_facts (e : ℕ) (he : e < (R.compose enc).toGraph.numEdges) : + ∃ (p : R.Dart) (z : Cube (ROf B)) (i : ReadIdx), + e = ((NumEnc.enc p.1 * NumEnc.card R.graph.D + NumEnc.enc p.2) * 2 ^ ROf B + + NumEnc.enc z) * 22 + NumEnc.enc i + ∧ ((R.compose enc).toGraph.tail ⟨e, he⟩).val = (R.compose enc).tailNum e + ∧ ((R.compose enc).toGraph.head ⟨e, he⟩).val + = posNum (NumEnc.card R.graph.V) (NumEnc.card R.graph.D) + (NumEnc.card (Cube B)) (NumEnc.card (Cube (nOf B))) + (NumEnc.card (Cube (nOf B * nOf B))) + (readKind i) (R.blockNum p i) (R.cubeNum enc p z i) + ∧ (R.compose enc).toGraph.rel ⟨e, he⟩ + = MultiTest.relOfCheck ((R.compose enc).check p z) i := by + obtain ⟨p, z, i, hp, hz, hi, hsplit⟩ := R.edge_data B enc e he + refine ⟨p, z, i, hsplit, (MultiTest.tailNum_eq _ ⟨e, he⟩).symm, ?_, ?_⟩ + · rw [hp, hz, hi, MultiTest.val_head_toGraph, enc_pos_compose] + rfl + · rw [hp, hz, hi] + exact MultiTest.rel_toGraph_eq _ _ + +end RegCSP + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgPreRel.lean b/Complexitylib/Classes/PCP/Internal/AlgPreRel.lean new file mode 100644 index 00000000..3fc60572 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgPreRel.lean @@ -0,0 +1,171 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.AlgGraph +public import Complexitylib.Classes.PCP.Internal.AlgPreprocess +public import Complexitylib.Classes.PCP.Internal.KilledCSP + +/-! +# The preprocessed constraint, in numbers + +Preprocessing leaves three kinds of constraint: the original one at an +edge-link, oriented by the half-edge's side; equality inside a cloud; and +nothing at all at a self-loop or an expander edge. So the constraint at a dart +is a fixed function of the dart's number, the half-edge's side, and the code of +the original constraint — a bounded amount of data. + +## Main definitions + +- `Complexity.preRelCode` — the constraint a dart's number and a code stand for + +## Main results + +- `Complexity.preRel_eq` — it is the preprocessed system's constraint +- `Complexity.rel_killedPow_preprocess` — the killed power's constraint runs + those codes along the walk +- `Complexity.rel_killedPow_eq_relOfSteps` — so it depends on the graph only + through the walk's parities and codes +- `Complexity.rel_killedPow_eq_preRelOfSteps` — the same, with every argument at + a type that does not mention the graph +-/ + +@[expose] public section + +namespace Complexity + +open NumEnc + +variable {α : Type} [Fintype α] [DecidableEq α] + +/-- The preprocessed system's constraint, from the dart's number `d`, the +half-edge's number `u` and the code `c` of the original constraint. -/ +noncomputable def preRelCode (α : Type) [Fintype α] [DecidableEq α] + (deg c u d : ℕ) (a b : α) : Bool := + if d = 0 then true + else if d = 1 then + (if u % 2 = 0 then relOfCode α c b a else relOfCode α c a b) + else if d < 2 + deg then decide (a = b) + else true + +/-- **The numbers give the preprocessed constraint.** -/ +theorem preRel_eq (G : ConstraintGraph α) (E : ExpanderFamily) (p : G.HalfEdge) + (d : (G.preprocess E).graph.D) (a b : α) : + (G.preprocess E).rel p d a b + = preRelCode α E.degree (codeOfRel (G.rel p.1)) (enc p) (enc d) a b := by + have hmod : enc p % 2 = (if p.2 then 0 else 1) := by + rw [ConstraintGraph.enc_halfEdge, ConstraintGraph.halfCode] + cases p.2 <;> simp + rcases G.preDart_cases E d with rfl | rfl | ⟨j, rfl⟩ | ⟨j, rfl⟩ + · rw [G.enc_preLoop E, preRelCode, if_pos rfl] + rfl + · rw [G.enc_preEdge E, preRelCode, if_neg one_ne_zero, if_pos rfl, relOfCode_codeOfRel, hmod] + show (if p.2 then G.rel p.1 b a else G.rel p.1 a b) = _ + cases hb : p.2 <;> simp + · have hj := j.isLt + rw [G.enc_preCloud E j, preRelCode, if_neg (by omega), if_neg (by omega), if_pos (by omega)] + rfl + · have hj := j.isLt + rw [G.enc_preExp E j, preRelCode, if_neg (by omega), if_neg (by omega), if_neg (by omega)] + rfl + +/-- Only the parity of a half-edge's number matters. -/ +theorem preRelCode_mod (deg c u d : ℕ) (a b : α) : + preRelCode α deg c (u % 2) d a b = preRelCode α deg c u d a b := by + rw [preRelCode, preRelCode, Nat.mod_mod_of_dvd u (dvd_refl 2)] + +/-- **The killed power's constraint, along the walk.** Each step contributes the +preprocessed constraint at the vertex it stands on, read off that vertex's +number and the code of the original constraint there. -/ +theorem rel_killedPow_preprocess (G : ConstraintGraph α) (E : ExpanderFamily) {q T : ℕ} + (hq : 0 < q) (v : (G.preprocess E).graph.V) + (x : (Fin T → (G.preprocess E).graph.D) × (Fin T → Fin q)) + (a b : KOpinion (G.preprocess E).graph T α) : + ((G.preprocess E).killedPow q T hq).rel v x a b + = decide (∀ i : Fin ((G.preprocess E).graph.kLen x), + preRelCode α E.degree + (codeOfRel (G.rel ((G.preprocess E).graph.walkAt + ((G.preprocess E).graph.kLen x) v ((G.preprocess E).graph.kWalk x) i.val).1)) + (enc ((G.preprocess E).graph.walkAt + ((G.preprocess E).graph.kLen x) v ((G.preprocess E).graph.kWalk x) i.val)) + (enc ((G.preprocess E).graph.kWalk x i)) + (a ((G.preprocess E).graph.startIdx ((G.preprocess E).graph.kLen_le x) + ((G.preprocess E).graph.kWalk x) i)) + (b ((G.preprocess E).graph.endIdx ((G.preprocess E).graph.kLen_le x) v + ((G.preprocess E).graph.kWalk x) i)) = true) := by + show decide (∀ i : Fin ((G.preprocess E).graph.kLen x), _ = true) = _ + simp only [preRel_eq] + rfl + +/-- What a killed dart's constraint runs: at each step, the dart it takes, the +parity of the vertex it stands on, the code of the constraint there, and where +the two ends hold their opinions about that step. -/ +noncomputable def relOfSteps {Gr : RegGraph} [NumEnc Gr.D] {T : ℕ} (deg n : ℕ) + (dart : Fin n → Gr.D) (par code : Fin n → ℕ) + (sIdx eIdx : Fin n → VarWalk Gr T) (a b : KOpinion Gr T α) : Bool := + decide (∀ i : Fin n, preRelCode α deg (code i) (par i) (NumEnc.enc (dart i)) + (a (sIdx i)) (b (eIdx i)) = true) + +/-- **The killed power's constraint depends on the graph only through the +walk's parities and codes** — a bounded amount of data. -/ +theorem rel_killedPow_eq_relOfSteps (G : ConstraintGraph α) (E : ExpanderFamily) {q T : ℕ} + (hq : 0 < q) (v : (G.preprocess E).graph.V) + (x : (Fin T → (G.preprocess E).graph.D) × (Fin T → Fin q)) : + ((G.preprocess E).killedPow q T hq).rel v x + = relOfSteps (Gr := (G.preprocess E).graph) E.degree + ((G.preprocess E).graph.kLen x) ((G.preprocess E).graph.kWalk x) + (fun i => enc ((G.preprocess E).graph.walkAt ((G.preprocess E).graph.kLen x) v + ((G.preprocess E).graph.kWalk x) i.val) % 2) + (fun i => codeOfRel (G.rel ((G.preprocess E).graph.walkAt + ((G.preprocess E).graph.kLen x) v ((G.preprocess E).graph.kWalk x) i.val).1)) + ((G.preprocess E).graph.startIdx ((G.preprocess E).graph.kLen_le x) + ((G.preprocess E).graph.kWalk x)) + ((G.preprocess E).graph.endIdx ((G.preprocess E).graph.kLen_le x) v + ((G.preprocess E).graph.kWalk x)) := by + funext a b + rw [rel_killedPow_preprocess, relOfSteps] + simp only [preRelCode_mod] + +/-! ### Types that do not mention the graph -/ + +/-- The darts of a preprocessed system: the self-loop, the edge-link, the +cloud's and the expander's. This is the dart type of `preprocess` for *every* +graph, so data about a preprocessed walk lives at a type that does not grow with +the input. -/ +abbrev PreDart (E : ExpanderFamily) : Type := Unit ⊕ (Option (Fin E.degree) ⊕ Fin E.degree) + +omit [Fintype α] in +/-- Walks of length at most `T` in a preprocessed system, likewise. -/ +abbrev PreWalk (E : ExpanderFamily) (T : ℕ) : Type := + Σ ℓ : Fin (T + 1), Fin ℓ.val → PreDart E + +omit [Fintype α] in +/-- The killed constraint, with every argument at a graph-free type. -/ +noncomputable def preRelOfSteps (E : ExpanderFamily) (T : ℕ) (deg n : ℕ) + (dart : Fin n → PreDart E) (par code : Fin n → ℕ) + (sIdx eIdx : Fin n → PreWalk E T) (a b : PreWalk E T → α) : Bool := + decide (∀ i : Fin n, preRelCode α deg (code i) (par i) (NumEnc.enc (dart i)) + (a (sIdx i)) (b (eIdx i)) = true) + +/-- **The killed power's constraint, as data at graph-free types.** Two graphs +whose walks show the same darts, parities, codes and opinion indices carry the +same constraint. -/ +theorem rel_killedPow_eq_preRelOfSteps (G : ConstraintGraph α) (E : ExpanderFamily) {q T : ℕ} + (hq : 0 < q) (v : (G.preprocess E).graph.V) + (x : (Fin T → (G.preprocess E).graph.D) × (Fin T → Fin q)) : + ((G.preprocess E).killedPow q T hq).rel v x + = preRelOfSteps E T E.degree ((G.preprocess E).graph.kLen x) + ((G.preprocess E).graph.kWalk x) + (fun i => enc ((G.preprocess E).graph.walkAt ((G.preprocess E).graph.kLen x) v + ((G.preprocess E).graph.kWalk x) i.val) % 2) + (fun i => codeOfRel (G.rel ((G.preprocess E).graph.walkAt + ((G.preprocess E).graph.kLen x) v ((G.preprocess E).graph.kWalk x) i.val).1)) + ((G.preprocess E).graph.startIdx ((G.preprocess E).graph.kLen_le x) + ((G.preprocess E).graph.kWalk x)) + ((G.preprocess E).graph.endIdx ((G.preprocess E).graph.kLen_le x) v + ((G.preprocess E).graph.kWalk x)) := + rel_killedPow_eq_relOfSteps G E hq v x + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgPreRot.lean b/Complexitylib/Classes/PCP/Internal/AlgPreRot.lean new file mode 100644 index 00000000..b4bdcb45 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgPreRot.lean @@ -0,0 +1,789 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.HeadOps +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.PCP.Internal.AlgGraph +public import Complexitylib.Classes.PCP.Internal.AlgPreprocess +public import Complexitylib.Classes.PCP.Internal.Materialize +public import Complexitylib.Classes.PCP.Internal.AlgFamily + +/-! +# Reading a half-edge's endpoint + +A half-edge is numbered `2 e` or `2 e + 1` according to which end of edge `e` it +is, so the vertex it hangs from is one of that edge's two endpoints — and both +are written down in the encoded graph. Halving the number picks the edge, its +parity picks the end. + +## Main definitions + +- `Complexity.ownerFn` — the vertex a half-edge number hangs from +- `Complexity.cloudSizeFn` — how many half-edges hang from a vertex +- `Complexity.cloudIdxFn` — how many of them come first + +## Main results + +- `Complexity.ownerFn_mem_FP`, `Complexity.ownerFn_eq` +- `Complexity.cloudSizeFn_mem_FP`, `Complexity.length_cloudSizeFn` — the count + is the number of half-edges the rule accepts +- `Complexity.ConstraintGraph.count_owner_eq_card_cloud` — and counting numbers + is counting half-edges +- `Complexity.cloudStepFn`, `Complexity.expStepFn` — the two moves that need the + expander +- `Complexity.preRotFn` — the preprocessed graph's rotation map, as one function + +## Main results + +- `Complexity.expStepFn_eq` — the expander move computes what it should +-/ + +@[expose] public section + +namespace Complexity + +/-- The vertex a half-edge hangs from, on `pair (encoded graph) (unary p)`. -/ +noncomputable def ownerFn (z : List Bool) : List Bool := + ifEqLen (modC 2 (pairSnd z)) [] + (recSnd (pairSnd (pairFst z)) (divC 2 (pairSnd z)).length) + (recFst (pairSnd (pairFst z)) (divC 2 (pairSnd z)).length) + +theorem ownerFn_mem_FP : ownerFn ∈ FP := by + have hp : (fun z : List Bool => pairSnd z) ∈ FP := Cobham.sndBlock_mem_FP + have hG : (fun z : List Bool => pairSnd (pairFst z)) ∈ FP := + mem_FP_of_eq (mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP) fun _ => rfl + have he := divC_mem_FP hp 2 + have hb := modC_mem_FP hp 2 + exact ifEqLen_mem_FP hb (constFn_mem_FP []) (recSnd_mem_FP he hG) (recFst_mem_FP he hG) + +/-! ### Counting a cloud -/ + +/-- One mark when the half-edge `j` hangs from the vertex asked for. The +argument is `pair (pair (encoded graph) (unary u)) (unary j)`. -/ +noncomputable def cloudMark (w : List Bool) : List Bool := + ifEqLen (ownerFn (pair (pairFst (pairFst w)) (pairSnd w))) + (pairSnd (pairFst w)) [true] [] + +theorem cloudMark_mem_FP : cloudMark ∈ FP := by + have hG : (fun w : List Bool => pairFst (pairFst w)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + have hu : (fun w : List Bool => pairSnd (pairFst w)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + have hj : (fun w : List Bool => pairSnd w) ∈ FP := Cobham.sndBlock_mem_FP + have hown : (fun w : List Bool => + ownerFn (pair (pairFst (pairFst w)) (pairSnd w))) ∈ FP := by + have h := mem_FP_comp (Cobham.pairFn_mem_FP hG hj) ownerFn_mem_FP + refine mem_FP_of_eq h fun w => ?_ + rw [Function.comp_apply] + exact ifEqLen_mem_FP hown hu (constFn_mem_FP [true]) (constFn_mem_FP []) + +theorem length_cloudMark (Gz u j : List Bool) : + (cloudMark (pair (pair Gz u) j)).length + = if (ownerFn (pair Gz j)).length = u.length then 1 else 0 := by + rw [cloudMark, pairFst_pair, pairSnd_pair, pairFst_pair, + pairSnd_pair] + by_cases h : (ownerFn (pair Gz j)).length = u.length + · rw [ifEqLen_pos h, if_pos h] + rfl + · rw [ifEqLen_neg h, if_neg h] + rfl + +/-- How many half-edges hang from the vertex asked for, on +`pair (encoded graph) (unary u)`. -/ +noncomputable def cloudSizeFn (z : List Bool) : List Bool := + countOver cloudMark + (pair (marks (mulC 2 (posCount (pairSnd (pairFst z))))) z) + +theorem cloudSizeFn_mem_FP : cloudSizeFn ∈ FP := by + have hcnt : (fun z : List Bool => + marks (mulC 2 (posCount (pairSnd (pairFst z))))) ∈ FP := by + have h1 : (fun z : List Bool => pairSnd (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + exact marks_mem_FP (mulC_mem_FP (posCount_mem_FP h1) 2) + have harg := Cobham.pairFn_mem_FP hcnt id_mem_FP + have h := mem_FP_comp harg (countOver_mem_FP cloudMark_mem_FP) + refine mem_FP_of_eq h fun w => ?_ + rw [Function.comp_apply, cloudSizeFn] + rfl + +/-- **The count is the number of half-edges the rule accepts.** -/ +theorem length_cloudSizeFn (Gz u : List Bool) (m : ℕ) + (hm : (mulC 2 (posCount (pairSnd Gz))).length = m) : + (cloudSizeFn (pair Gz u)).length + = ∑ j ∈ Finset.range m, + (if (ownerFn (pair Gz (List.replicate j true))).length = u.length then 1 else 0) := by + have hmarks : marks (mulC 2 (posCount (pairSnd Gz))) = List.replicate m true := by + rw [marks_eq, hm] + rw [cloudSizeFn, pairFst_pair, hmarks, length_countOver] + refine Finset.sum_congr rfl fun j _ => ?_ + rw [length_cloudMark] + +/-- How many half-edges before this one hang from the same vertex, on +`pair (encoded graph) (unary p)`. -/ +noncomputable def cloudIdxFn (z : List Bool) : List Bool := + countOver cloudMark + (pair (marks (pairSnd z)) (pair (pairFst z) (ownerFn z))) + +theorem cloudIdxFn_mem_FP : cloudIdxFn ∈ FP := by + have hcnt := marks_mem_FP Cobham.sndBlock_mem_FP + have hdata := Cobham.pairFn_mem_FP Cobham.fstBlock_mem_FP ownerFn_mem_FP + have h := mem_FP_comp (Cobham.pairFn_mem_FP hcnt hdata) + (countOver_mem_FP cloudMark_mem_FP) + refine mem_FP_of_eq h fun w => ?_ + rw [Function.comp_apply, cloudIdxFn] + +/-- **The index is the number of earlier half-edges in the same cloud.** -/ +theorem length_cloudIdxFn (Gz : List Bool) (p : ℕ) : + (cloudIdxFn (pair Gz (List.replicate p true))).length + = ∑ j ∈ Finset.range p, + (if (ownerFn (pair Gz (List.replicate j true))).length + = (ownerFn (pair Gz (List.replicate p true))).length then 1 else 0) := by + have hmarks : marks (pairSnd (pair Gz (List.replicate p true))) + = List.replicate p true := by + rw [pairSnd_pair, marks_eq, List.length_replicate] + rw [cloudIdxFn, hmarks, pairFst_pair, length_countOver] + refine Finset.sum_congr rfl fun j _ => ?_ + rw [length_cloudMark] + +/-! ### Finding a cloud's members -/ + + + +/-- One mark when the half-edge `c` is the `k`-th of the cloud of `u`. The +argument is `pair (pair (encoded graph) (pair (unary u) (unary k))) (unary c)`. -/ +noncomputable def eltMark (w : List Bool) : List Bool := + ifEqLen (ownerFn (pair (pairFst (pairFst w)) (pairSnd w))) + (pairFst (pairSnd (pairFst w))) + (ifEqLen (cloudIdxFn (pair (pairFst (pairFst w)) (pairSnd w))) + (pairSnd (pairSnd (pairFst w))) [true] []) + [] + +theorem eltMark_mem_FP : eltMark ∈ FP := by + have hG : (fun w : List Bool => pairFst (pairFst w)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + have hc : (fun w : List Bool => pairSnd w) ∈ FP := Cobham.sndBlock_mem_FP + have hu : (fun w : List Bool => + pairFst (pairSnd (pairFst w))) ∈ FP := + mem_FP_comp (mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP) + Cobham.fstBlock_mem_FP + have hk : (fun w : List Bool => + pairSnd (pairSnd (pairFst w))) ∈ FP := + mem_FP_comp (mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP) + Cobham.sndBlock_mem_FP + have harg := Cobham.pairFn_mem_FP hG hc + have hown : (fun w : List Bool => + ownerFn (pair (pairFst (pairFst w)) (pairSnd w))) ∈ FP := by + have h := mem_FP_comp harg ownerFn_mem_FP + refine mem_FP_of_eq h fun w => ?_ + rw [Function.comp_apply] + have hidx : (fun w : List Bool => + cloudIdxFn (pair (pairFst (pairFst w)) (pairSnd w))) ∈ FP := by + have h := mem_FP_comp harg cloudIdxFn_mem_FP + refine mem_FP_of_eq h fun w => ?_ + rw [Function.comp_apply] + exact ifEqLen_mem_FP hown hu + (ifEqLen_mem_FP hidx hk (constFn_mem_FP [true]) (constFn_mem_FP [])) + (constFn_mem_FP []) + +theorem length_eltMark (Gz u k c : List Bool) : + (eltMark (pair (pair Gz (pair u k)) c)).length + = if (ownerFn (pair Gz c)).length = u.length then + (if (cloudIdxFn (pair Gz c)).length = k.length then 1 else 0) + else 0 := by + rw [eltMark, pairFst_pair, pairSnd_pair, pairFst_pair, + pairSnd_pair, pairFst_pair, pairSnd_pair] + by_cases h1 : (ownerFn (pair Gz c)).length = u.length + · rw [ifEqLen_pos h1, if_pos h1] + by_cases h2 : (cloudIdxFn (pair Gz c)).length = k.length + · rw [ifEqLen_pos h2, if_pos h2] + rfl + · rw [ifEqLen_neg h2, if_neg h2] + rfl + · rw [ifEqLen_neg h1, if_neg h1] + rfl + +/-- The `k`-th half-edge of the cloud of `u`, on +`pair (encoded graph) (pair (unary u) (unary k))`. -/ +noncomputable def cloudEltFn (z : List Bool) : List Bool := + findFirst eltMark + (pair (marks (mulC 2 (posCount (pairSnd (pairFst z))))) z) + +theorem cloudEltFn_eq_replicate (z : List Bool) : + cloudEltFn z = List.replicate (cloudEltFn z).length true := by + conv_lhs => rw [cloudEltFn, findFirst_eq_replicate] + rw [← cloudEltFn] + +theorem cloudEltFn_mem_FP : cloudEltFn ∈ FP := by + have h1 : (fun z : List Bool => pairSnd (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + have hcnt := marks_mem_FP (mulC_mem_FP (posCount_mem_FP h1) 2) + have h := mem_FP_comp (Cobham.pairFn_mem_FP hcnt id_mem_FP) + (findFirst_mem_FP eltMark_mem_FP) + refine mem_FP_of_eq h fun w => ?_ + rw [Function.comp_apply, cloudEltFn] + rfl + +variable {α : Type} [Fintype α] [DecidableEq α] + +/-- **The reading is the owner.** -/ +theorem ownerFn_eq (G : ConstraintGraph α) (p : ℕ) (hp : p / 2 < G.numEdges) : + ownerFn (pair (encGraph G) (List.replicate p true)) + = List.replicate (G.ownerNum p) true := by + have hdiv : (divC 2 (List.replicate p true)) = List.replicate (p / 2) true := by + rw [divC_eq (by norm_num), List.length_replicate] + have hmod : (modC 2 (List.replicate p true)) = List.replicate (p % 2) true := by + rw [modC_eq (by norm_num), List.length_replicate] + rw [ownerFn, pairSnd_pair, pairFst_pair, hdiv, hmod, + List.length_replicate, ConstraintGraph.ownerNum, dif_pos hp] + by_cases h : p % 2 = 0 + · rw [if_pos h, ifEqLen_pos (by rw [h]; rfl)] + rw [encGraph, pairSnd_pair, + recSnd_eq (l3 := edgeRecs G) (by rw [length_edgeRecs]; exact hp) + (getElem_edgeRecs G _ hp)] + · rw [if_neg h, ifEqLen_neg (by + rw [List.length_replicate, List.length_nil] + exact h)] + rw [encGraph, pairSnd_pair, + recFst_eq (l3 := edgeRecs G) (by rw [length_edgeRecs]; exact hp) + (getElem_edgeRecs G _ hp)] + +namespace ConstraintGraph + +omit [Fintype α] [DecidableEq α] in +/-- **Counting numbers is counting half-edges.** -/ +theorem count_owner_eq_card_cloud (G : ConstraintGraph α) (v : Fin G.numVerts) : + ((Finset.range (2 * G.numEdges)).filter fun j => G.ownerNum j = v.val).card + = (G.cloud v).card := by + classical + refine (Finset.card_bij (fun p _ => NumEnc.enc p) ?_ ?_ ?_).symm + · intro p hp + have howner : G.owner p = v := (G.mem_cloud).mp hp + refine Finset.mem_filter.mpr ⟨Finset.mem_range.mpr ?_, ?_⟩ + · show NumEnc.enc p < 2 * G.numEdges + rw [enc_halfEdge, halfCode] + have he := p.1.isLt + cases p.2 + · simp + omega + · simp + · show G.ownerNum (NumEnc.enc p) = v.val + rw [G.ownerNum_enc p, howner] + · intro p _ q _ h + exact NumEnc.enc_injective h + · intro j hj + rw [Finset.mem_filter, Finset.mem_range] at hj + have he : j / 2 < G.numEdges := by omega + refine ⟨(⟨j / 2, he⟩, decide (j % 2 = 0)), ?_, ?_⟩ + · have hcode : NumEnc.enc ((⟨j / 2, he⟩, decide (j % 2 = 0)) : G.HalfEdge) = j := by + rw [enc_halfEdge, halfCode] + by_cases h2 : j % 2 = 0 <;> simp [h2] <;> omega + have hown := G.ownerNum_enc ((⟨j / 2, he⟩, decide (j % 2 = 0)) : G.HalfEdge) + rw [hcode] at hown + exact (G.mem_cloud).mpr (Fin.ext (hown.symm.trans hj.2)) + · show NumEnc.enc ((⟨j / 2, he⟩, decide (j % 2 = 0)) : G.HalfEdge) = j + rw [enc_halfEdge, halfCode] + by_cases h2 : j % 2 = 0 <;> simp [h2] <;> omega + +omit [Fintype α] [DecidableEq α] in +/-- **Counting numbers below a bound is counting codes below it.** -/ +theorem count_owner_lt_eq_countBelow (G : ConstraintGraph α) (v : Fin G.numVerts) (m : ℕ) + (hm : m ≤ 2 * G.numEdges) : + ((Finset.range m).filter fun j => G.ownerNum j = v.val).card + = countBelow (G.cloudCodes v) m := by + classical + rw [G.countBelow_cloudCodes v m] + refine (Finset.card_bij (fun p _ => NumEnc.enc p) ?_ ?_ ?_).symm + · intro p hp + rw [Finset.mem_filter] at hp + have howner : G.owner p = v := (G.mem_cloud).mp hp.1 + refine Finset.mem_filter.mpr ⟨Finset.mem_range.mpr ?_, ?_⟩ + · show NumEnc.enc p < m + rw [enc_halfEdge] + exact hp.2 + · show G.ownerNum (NumEnc.enc p) = v.val + rw [G.ownerNum_enc p, howner] + · intro p _ q _ h + exact NumEnc.enc_injective h + · intro j hj + rw [Finset.mem_filter, Finset.mem_range] at hj + have he : j / 2 < G.numEdges := by omega + refine ⟨(⟨j / 2, he⟩, decide (j % 2 = 0)), ?_, ?_⟩ + · have hcode : NumEnc.enc ((⟨j / 2, he⟩, decide (j % 2 = 0)) : G.HalfEdge) = j := by + rw [enc_halfEdge, halfCode] + by_cases h2 : j % 2 = 0 <;> simp [h2] <;> omega + have hown := G.ownerNum_enc ((⟨j / 2, he⟩, decide (j % 2 = 0)) : G.HalfEdge) + rw [hcode] at hown + refine Finset.mem_filter.mpr ⟨(G.mem_cloud).mpr (Fin.ext (hown.symm.trans hj.2)), ?_⟩ + rw [← enc_halfEdge, hcode] + exact hj.1 + · show NumEnc.enc ((⟨j / 2, he⟩, decide (j % 2 = 0)) : G.HalfEdge) = j + rw [enc_halfEdge, halfCode] + by_cases h2 : j % 2 = 0 <;> simp [h2] <;> omega + +end ConstraintGraph + +/-! ### What the counts compute -/ + +variable (G : ConstraintGraph α) + +theorem length_count_encGraph : + (mulC 2 (posCount (pairSnd (encGraph G)))).length = 2 * G.numEdges := by + rw [encGraph, pairSnd_pair, posCount_eq, length_mulC, List.length_replicate, + length_edgeRecs] + ring + +/-- **The size the algorithm counts is the size of the cloud.** -/ +theorem length_cloudSizeFn_eq (v : Fin G.numVerts) : + (cloudSizeFn (pair (encGraph G) (List.replicate v.val true))).length + = (G.cloud v).card := by + classical + rw [length_cloudSizeFn _ _ _ (length_count_encGraph G)] + rw [← ConstraintGraph.count_owner_eq_card_cloud G v, Finset.card_filter] + refine Finset.sum_congr rfl fun j hj => ?_ + rw [Finset.mem_range] at hj + have hje : j / 2 < G.numEdges := by omega + rw [ownerFn_eq G j hje, List.length_replicate, List.length_replicate] + +/-- **The index the algorithm counts is the position in the cloud.** -/ +theorem length_cloudIdxFn_eq (m : ℕ) (hm : m < 2 * G.numEdges) : + (cloudIdxFn (pair (encGraph G) (List.replicate m true))).length + = countBelow (G.cloudCodes ⟨G.ownerNum m, by + have hme : m / 2 < G.numEdges := by omega + rw [ConstraintGraph.ownerNum, dif_pos hme] + split <;> exact Fin.isLt _⟩) m := by + classical + have hme : m / 2 < G.numEdges := by omega + rw [length_cloudIdxFn, ← ConstraintGraph.count_owner_lt_eq_countBelow G _ m (by omega), + Finset.card_filter] + refine Finset.sum_congr rfl fun j hj => ?_ + rw [Finset.mem_range] at hj + have hje : j / 2 < G.numEdges := by omega + rw [ownerFn_eq G j hje, ownerFn_eq G m hme, List.length_replicate, List.length_replicate] + +omit [Fintype α] [DecidableEq α] in +/-- The code of a half-edge is below twice the edge count. -/ +theorem halfCode_lt (p : G.HalfEdge) : G.halfCode p < 2 * G.numEdges := by + rw [ConstraintGraph.halfCode] + by_cases hb : p.2 = true + · rw [if_pos hb] + omega + · rw [if_neg hb] + omega + +/-- **The search finds the `k`-th half-edge of the cloud.** -/ +theorem length_cloudEltFn_eq (v : Fin G.numVerts) (k : ℕ) + (hk : k < (G.cloudList v).length) : + (cloudEltFn (pair (encGraph G) (pair (List.replicate v.val true) + (List.replicate k true)))).length + = G.halfCode ((G.cloudList v)[k]) := by + classical + set q : G.HalfEdge := (G.cloudList v)[k] with hq + have hmem : q ∈ G.cloudList v := List.getElem_mem hk + have howner : G.owner q = v := (G.mem_cloud).mp ((G.mem_cloudList).mp hmem) + have hidx : (G.cloudList v).idxOf q = k := (G.nodup_cloudList v).idxOf_getElem _ hk + have hcount : countBelow (G.cloudCodes v) (G.halfCode q) = k := by + rw [← G.idxOf_cloudList howner, hidx] + have hclt : G.halfCode q < 2 * G.numEdges := halfCode_lt G q + have hmarks : marks (mulC 2 (posCount (pairSnd (encGraph G)))) + = List.replicate (2 * G.numEdges) true := by + rw [marks_eq, length_count_encGraph] + rw [cloudEltFn, pairFst_pair, hmarks] + refine length_findFirst_eq hclt ?_ ?_ + · have howner' : G.ownerNum (G.halfCode q) = v.val := by + rw [← ConstraintGraph.enc_halfEdge, ConstraintGraph.ownerNum_enc, howner] + have hown1 : (ownerFn (pair (encGraph G) (List.replicate (G.halfCode q) true))).length + = (List.replicate v.val true).length := by + rw [ownerFn_eq G _ (by omega), List.length_replicate, List.length_replicate, howner'] + have hidx1 : (cloudIdxFn (pair (encGraph G) (List.replicate (G.halfCode q) true))).length + = (List.replicate k true).length := by + rw [length_cloudIdxFn_eq G _ hclt, List.length_replicate] + have hv : (⟨G.ownerNum (G.halfCode q), by + have hme : G.halfCode q / 2 < G.numEdges := by omega + rw [ConstraintGraph.ownerNum, dif_pos hme] + split <;> exact Fin.isLt _⟩ : Fin G.numVerts) = v := Fin.ext howner' + rw [hv, hcount] + rw [length_eltMark, if_pos hown1, if_pos hidx1] + omega + · intro j hj + rw [length_eltMark] + by_cases h1 : (ownerFn (pair (encGraph G) (List.replicate j true))).length + = (List.replicate v.val true).length + · rw [if_pos h1, if_neg ?_] + have hjlt : j < 2 * G.numEdges := by omega + have hjown : G.ownerNum j = v.val := by + rw [ownerFn_eq G j (by omega), List.length_replicate, List.length_replicate] at h1 + exact h1 + have hjmem : j ∈ G.cloudCodes v := by + refine (G.mem_cloudCodes).mpr ⟨(⟨j / 2, by omega⟩, decide (j % 2 = 0)), ?_, ?_⟩ + · have hcode : NumEnc.enc ((⟨j / 2, by omega⟩, decide (j % 2 = 0)) : G.HalfEdge) = j := by + rw [ConstraintGraph.enc_halfEdge, ConstraintGraph.halfCode] + by_cases h2 : j % 2 = 0 <;> simp [h2] <;> omega + have hown := G.ownerNum_enc ((⟨j / 2, by omega⟩, decide (j % 2 = 0)) : G.HalfEdge) + rw [hcode] at hown + exact Fin.ext (hown.symm.trans hjown) + · rw [ConstraintGraph.halfCode] + by_cases h2 : j % 2 = 0 <;> simp [h2] <;> omega + have hlt := countBelow_lt_countBelow hjmem hj + rw [hcount] at hlt + rw [length_cloudIdxFn_eq G _ hjlt, List.length_replicate] + have hjv : (⟨G.ownerNum j, by + have hme : j / 2 < G.numEdges := by omega + rw [ConstraintGraph.ownerNum, dif_pos hme] + split <;> exact Fin.isLt _⟩ : Fin G.numVerts) = v := Fin.ext hjown + rw [hjv] + omega + · rw [if_neg h1] + +/-! ### The two moves that need the expander -/ + +variable (F : FinBase) (pol : Polynomial ℕ) + +/-- The cloud move, on `pair (pair (graph) (unary owner)) (pair (unary code) +(unary dart))`: rotate the half-edge's index inside its cloud, then read off the +half-edge the new index names. -/ +noncomputable def cloudStepFn (z : List Bool) : List Bool := + pair + (cloudEltFn (pair (pairFst (pairFst z)) + (pair (pairSnd (pairFst z)) + (pairFst (F.famRotFn pol + (pair (cloudSizeFn (pair (pairFst (pairFst z)) + (pairSnd (pairFst z)))) + (pair (cloudIdxFn (pair (pairFst (pairFst z)) + (pairFst (pairSnd z)))) + (pairSnd (pairSnd z))))))))) + (pairSnd (F.famRotFn pol + (pair (cloudSizeFn (pair (pairFst (pairFst z)) + (pairSnd (pairFst z)))) + (pair (cloudIdxFn (pair (pairFst (pairFst z)) + (pairFst (pairSnd z)))) + (pairSnd (pairSnd z)))))) + +theorem cloudStepFn_mem_FP : cloudStepFn F pol ∈ FP := by + have hG : (fun z : List Bool => pairFst (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + have hu : (fun z : List Bool => pairSnd (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + have hc : (fun z : List Bool => pairFst (pairSnd z)) ∈ FP := + mem_FP_comp Cobham.sndBlock_mem_FP Cobham.fstBlock_mem_FP + have hj : (fun z : List Bool => pairSnd (pairSnd z)) ∈ FP := + mem_FP_comp Cobham.sndBlock_mem_FP Cobham.sndBlock_mem_FP + have hsize : (fun z : List Bool => cloudSizeFn (pair (pairFst (pairFst z)) + (pairSnd (pairFst z)))) ∈ FP := by + have h := mem_FP_comp (Cobham.pairFn_mem_FP hG hu) cloudSizeFn_mem_FP + refine mem_FP_of_eq h fun w => ?_ + rw [Function.comp_apply] + have hidx : (fun z : List Bool => cloudIdxFn (pair (pairFst (pairFst z)) + (pairFst (pairSnd z)))) ∈ FP := by + have h := mem_FP_comp (Cobham.pairFn_mem_FP hG hc) cloudIdxFn_mem_FP + refine mem_FP_of_eq h fun w => ?_ + rw [Function.comp_apply] + have hy : (fun z : List Bool => F.famRotFn pol + (pair (cloudSizeFn (pair (pairFst (pairFst z)) + (pairSnd (pairFst z)))) + (pair (cloudIdxFn (pair (pairFst (pairFst z)) + (pairFst (pairSnd z)))) + (pairSnd (pairSnd z))))) ∈ FP := by + have h := mem_FP_comp (Cobham.pairFn_mem_FP hsize (Cobham.pairFn_mem_FP hidx hj)) + (F.famRotFn_mem_FP pol) + refine mem_FP_of_eq h fun w => ?_ + rw [Function.comp_apply] + have helt : (fun z : List Bool => cloudEltFn (pair (pairFst (pairFst z)) + (pair (pairSnd (pairFst z)) + (pairFst (F.famRotFn pol + (pair (cloudSizeFn (pair (pairFst (pairFst z)) + (pairSnd (pairFst z)))) + (pair (cloudIdxFn (pair (pairFst (pairFst z)) + (pairFst (pairSnd z)))) + (pairSnd (pairSnd z))))))))) ∈ FP := by + have h := mem_FP_comp (Cobham.pairFn_mem_FP hG + (Cobham.pairFn_mem_FP hu (mem_FP_comp hy Cobham.fstBlock_mem_FP))) cloudEltFn_mem_FP + refine mem_FP_of_eq h fun w => ?_ + simp only [Function.comp_apply] + have hout := Cobham.pairFn_mem_FP helt (mem_FP_comp hy Cobham.sndBlock_mem_FP) + refine mem_FP_of_eq hout fun w => ?_ + simp only [Function.comp_apply] + rw [cloudStepFn] + +/-- The expander move, on `pair (graph) (pair (unary vertex) (unary dart))`. -/ +noncomputable def expStepFn (z : List Bool) : List Bool := + F.famRotFn pol + (pair (marks (mulC 2 (posCount (pairSnd (pairFst z))))) + (pairSnd z)) + +theorem expStepFn_mem_FP : expStepFn F pol ∈ FP := by + have h1 : (fun z : List Bool => pairSnd (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + have hcnt := marks_mem_FP (mulC_mem_FP (posCount_mem_FP h1) 2) + have h := mem_FP_comp (Cobham.pairFn_mem_FP hcnt Cobham.sndBlock_mem_FP) + (F.famRotFn_mem_FP pol) + refine mem_FP_of_eq h fun w => ?_ + rw [Function.comp_apply, expStepFn] + +/-- **The cloud move computes what it should.** -/ +theorem cloudStepFn_eq (hd : 1 < F.deg) (v : Fin G.numVerts) (c j : ℕ) + (hc : c < 2 * G.numEdges) (hown : G.ownerNum c = v.val) + (hj : j < (F.toFamily hd).degree) + (hp : F.fitLevel hd (G.cloudList v).length ≤ pol.eval (G.cloudList v).length) : + cloudStepFn F pol (pair (pair (encGraph G) (List.replicate v.val true)) + (pair (List.replicate c true) (List.replicate j true))) + = pair (List.replicate (G.cloudStepNum (F.toFamily hd) v c ⟨j, hj⟩).1 true) + (List.replicate (G.cloudStepNum (F.toFamily hd) v c ⟨j, hj⟩).2 true) := by + classical + have hce : c / 2 < G.numEdges := by omega + have hcmem : c ∈ G.cloudCodes v := by + refine (G.mem_cloudCodes).mpr ⟨(⟨c / 2, hce⟩, decide (c % 2 = 0)), ?_, ?_⟩ + · have hcode : NumEnc.enc ((⟨c / 2, hce⟩, decide (c % 2 = 0)) : G.HalfEdge) = c := by + rw [ConstraintGraph.enc_halfEdge, ConstraintGraph.halfCode] + by_cases h2 : c % 2 = 0 <;> simp [h2] <;> omega + have hown' := G.ownerNum_enc ((⟨c / 2, hce⟩, decide (c % 2 = 0)) : G.HalfEdge) + rw [hcode] at hown' + exact Fin.ext (hown'.symm.trans hown) + · rw [ConstraintGraph.halfCode] + by_cases h2 : c % 2 = 0 <;> simp [h2] <;> omega + have hidxlt : countBelow (G.cloudCodes v) c < (G.cloudList v).length := by + rw [← G.card_cloudCodes_eq_length v] + exact countBelow_lt_card hcmem + have hpos : 0 < (G.cloudList v).length := by omega + have hsizelen : (cloudSizeFn (pair (encGraph G) (List.replicate v.val true))).length + = (G.cloudList v).length := by + rw [length_cloudSizeFn_eq, ConstraintGraph.length_cloudList] + have hsize : cloudSizeFn (pair (encGraph G) (List.replicate v.val true)) + = List.replicate (G.cloudList v).length true := by + conv_lhs => rw [cloudSizeFn, countOver_eq_replicate] + rw [← cloudSizeFn, hsizelen] + have hvfin : (⟨G.ownerNum c, by + rw [ConstraintGraph.ownerNum, dif_pos hce] + split <;> exact Fin.isLt _⟩ : Fin G.numVerts) = v := Fin.ext hown + have hidxlen : (cloudIdxFn (pair (encGraph G) (List.replicate c true))).length + = countBelow (G.cloudCodes v) c := by + rw [length_cloudIdxFn_eq G _ hc, hvfin] + have hidx : cloudIdxFn (pair (encGraph G) (List.replicate c true)) + = List.replicate (countBelow (G.cloudCodes v) c) true := by + conv_lhs => rw [cloudIdxFn, countOver_eq_replicate] + rw [← cloudIdxFn, hidxlen] + have hval : F.famRotVal hd (G.cloudList v).length (countBelow (G.cloudCodes v) c, j) + = (((F.toFamily hd).rot (G.cloudList v).length + ((⟨countBelow (G.cloudCodes v) c, hidxlt⟩ : Fin (G.cloudList v).length), + (⟨j, hj⟩ : Fin (F.toFamily hd).degree))).1.val, + ((F.toFamily hd).rot (G.cloudList v).length + ((⟨countBelow (G.cloudCodes v) c, hidxlt⟩ : Fin (G.cloudList v).length), + (⟨j, hj⟩ : Fin (F.toFamily hd).degree))).2.val) := + F.famRotVal_eq hd hpos (⟨_, hidxlt⟩ : Fin (G.cloudList v).length) ⟨j, hj⟩ + have hrot := F.famRotFn_eq pol hd (G.cloudList v).length + (countBelow (G.cloudCodes v) c) j hpos hp + rw [hval] at hrot + have helt : cloudEltFn (pair (encGraph G) (pair (List.replicate v.val true) + (List.replicate ((F.toFamily hd).rot (G.cloudList v).length + ((⟨countBelow (G.cloudCodes v) c, hidxlt⟩ : Fin (G.cloudList v).length), + (⟨j, hj⟩ : Fin (F.toFamily hd).degree))).1.val true))) + = List.replicate (G.halfCode ((G.cloudList v)[((F.toFamily hd).rot + (G.cloudList v).length + ((⟨countBelow (G.cloudCodes v) c, hidxlt⟩ : Fin (G.cloudList v).length), + (⟨j, hj⟩ : Fin (F.toFamily hd).degree))).1.val])) true := by + conv_lhs => rw [cloudEltFn_eq_replicate] + rw [length_cloudEltFn_eq G v _ (Fin.isLt _)] + rw [cloudStepFn] + simp only [pairFst_pair, pairSnd_pair] + rw [hsize, hidx, hrot] + simp only [pairFst_pair, pairSnd_pair] + rw [helt, ConstraintGraph.cloudStepNum, dif_pos hidxlt] + dsimp only + rw [← G.halfCode_getElem_cloudList v _ (Fin.isLt _)] + +/-- **The expander move computes what it should.** -/ +theorem expStepFn_eq (hd : 1 < F.deg) (v j : ℕ) (hn : 0 < 2 * G.numEdges) + (hp : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) : + expStepFn F pol (pair (encGraph G) + (pair (List.replicate v true) (List.replicate j true))) + = pair (List.replicate (F.famRotVal hd (2 * G.numEdges) (v, j)).1 true) + (List.replicate (F.famRotVal hd (2 * G.numEdges) (v, j)).2 true) := by + have hmarks : marks (mulC 2 (posCount (pairSnd (encGraph G)))) + = List.replicate (2 * G.numEdges) true := by + rw [marks_eq, length_count_encGraph] + rw [expStepFn, pairFst_pair, pairSnd_pair, hmarks] + exact F.famRotFn_eq pol hd _ v j hn hp + +/-! ### The whole rotation map -/ + +/-- Crossing an edge: flip the last bit of the vertex number. -/ +noncomputable def flipFn (v : List Bool) : List Bool := + ifEqLen (modC 2 v) [] (v ++ [true]) (dropOne v) + +theorem flipFn_mem_FP {f : List Bool → List Bool} (hf : f ∈ FP) : + (fun z => flipFn (f z)) ∈ FP := + ifEqLen_mem_FP (modC_mem_FP hf 2) (constFn_mem_FP []) + (Cobham.appendFn_mem_FP hf (constFn_mem_FP [true])) (dropOneFn_mem_FP hf) + +theorem flipFn_eq (v : ℕ) : + flipFn (List.replicate v true) + = List.replicate (if v % 2 = 0 then v + 1 else v - 1) true := by + by_cases h : v % 2 = 0 + · rw [flipFn, modC_eq (by norm_num), List.length_replicate, h, + ifEqLen_pos (by simp)] + simp [List.replicate_succ'] + · rw [flipFn, modC_eq (by norm_num), List.length_replicate, + ifEqLen_neg (by simp [h]), if_neg h, dropOne] + simp + +/-- **The preprocessed graph's rotation map**, on +`pair (graph) (pair (unary vertex) (unary dart))`. Dart `0` is the self-loop, +dart `1` crosses the edge, the next `deg` are the cloud's, and the rest are the +superposed expander's. -/ +noncomputable def preRotFn (deg : ℕ) (z : List Bool) : List Bool := + ifEqLen (pairSnd (pairSnd z)) [] + (pair (pairFst (pairSnd z)) []) + (ifEqLen (pairSnd (pairSnd z)) [true] + (pair (flipFn (pairFst (pairSnd z))) [true]) + (ifLtLen (pairSnd (pairSnd z)) (List.replicate (2 + deg) true) + (pair + (pairFst (cloudStepFn F pol + (pair (pair (pairFst z) + (ownerFn (pair (pairFst z) (pairFst (pairSnd z))))) + (pair (pairFst (pairSnd z)) + ((pairSnd (pairSnd z)).drop 2))))) + (pairSnd (cloudStepFn F pol + (pair (pair (pairFst z) + (ownerFn (pair (pairFst z) (pairFst (pairSnd z))))) + (pair (pairFst (pairSnd z)) + ((pairSnd (pairSnd z)).drop 2)))) ++ [true, true])) + (pair + (pairFst (expStepFn F pol + (pair (pairFst z) + (pair (pairFst (pairSnd z)) + ((pairSnd (pairSnd z)).drop (2 + deg)))))) + (pairSnd (expStepFn F pol + (pair (pairFst z) + (pair (pairFst (pairSnd z)) + ((pairSnd (pairSnd z)).drop (2 + deg))))) + ++ List.replicate (2 + deg) true)))) + +theorem preRotFn_mem_FP (deg : ℕ) : preRotFn F pol deg ∈ FP := by + have hG : (fun z : List Bool => pairFst z) ∈ FP := Cobham.fstBlock_mem_FP + have hv : (fun z : List Bool => pairFst (pairSnd z)) ∈ FP := + mem_FP_comp Cobham.sndBlock_mem_FP Cobham.fstBlock_mem_FP + have hd : (fun z : List Bool => pairSnd (pairSnd z)) ∈ FP := + mem_FP_comp Cobham.sndBlock_mem_FP Cobham.sndBlock_mem_FP + have hd2 : (fun z : List Bool => (pairSnd (pairSnd z)).drop 2) ∈ FP := by + have := dropLenFn_mem_FP (constFn_mem_FP (List.replicate 2 true)) hd + refine mem_FP_of_eq this fun w => ?_ + rw [List.length_replicate] + have hdk : (fun z : List Bool => + (pairSnd (pairSnd z)).drop (2 + deg)) ∈ FP := by + have := dropLenFn_mem_FP (constFn_mem_FP (List.replicate (2 + deg) true)) hd + refine mem_FP_of_eq this fun w => ?_ + rw [List.length_replicate] + have hown : (fun z : List Bool => + ownerFn (pair (pairFst z) (pairFst (pairSnd z)))) ∈ FP := by + have h := mem_FP_comp (Cobham.pairFn_mem_FP hG hv) ownerFn_mem_FP + refine mem_FP_of_eq h fun w => ?_ + simp only [Function.comp_apply] + have hcs : (fun z : List Bool => cloudStepFn F pol + (pair (pair (pairFst z) + (ownerFn (pair (pairFst z) (pairFst (pairSnd z))))) + (pair (pairFst (pairSnd z)) + ((pairSnd (pairSnd z)).drop 2)))) ∈ FP := by + have h := mem_FP_comp (Cobham.pairFn_mem_FP (Cobham.pairFn_mem_FP hG hown) + (Cobham.pairFn_mem_FP hv hd2)) (cloudStepFn_mem_FP F pol) + refine mem_FP_of_eq h fun w => ?_ + simp only [Function.comp_apply] + have hes : (fun z : List Bool => expStepFn F pol + (pair (pairFst z) + (pair (pairFst (pairSnd z)) + ((pairSnd (pairSnd z)).drop (2 + deg))))) ∈ FP := by + have h := mem_FP_comp (Cobham.pairFn_mem_FP hG (Cobham.pairFn_mem_FP hv hdk)) + (expStepFn_mem_FP F pol) + refine mem_FP_of_eq h fun w => ?_ + simp only [Function.comp_apply] + have hloop := Cobham.pairFn_mem_FP hv (constFn_mem_FP []) + have hflip := Cobham.pairFn_mem_FP (flipFn_mem_FP hv) (constFn_mem_FP [true]) + have hcloud := Cobham.pairFn_mem_FP (mem_FP_comp hcs Cobham.fstBlock_mem_FP) + (Cobham.appendFn_mem_FP (mem_FP_comp hcs Cobham.sndBlock_mem_FP) + (constFn_mem_FP [true, true])) + have hexp := Cobham.pairFn_mem_FP (mem_FP_comp hes Cobham.fstBlock_mem_FP) + (Cobham.appendFn_mem_FP (mem_FP_comp hes Cobham.sndBlock_mem_FP) + (constFn_mem_FP (List.replicate (2 + deg) true))) + have hinner := ifLtLen_mem_FP hd (constFn_mem_FP (List.replicate (2 + deg) true)) + hcloud hexp + have houter := ifEqLen_mem_FP hd (constFn_mem_FP []) hloop + (ifEqLen_mem_FP hd (constFn_mem_FP [true]) hflip hinner) + refine mem_FP_of_eq houter fun w => ?_ + simp only [Function.comp_apply] + rw [preRotFn] + +/-- **The whole rotation map computes what it should.** -/ +theorem preRotFn_eq (hd : 1 < F.deg) (v d : ℕ) (hv : v < 2 * G.numEdges) + (hdlt : d < 2 + 2 * (F.toFamily hd).degree) + (hpc : ∀ u : Fin G.numVerts, + F.fitLevel hd (G.cloudList u).length ≤ pol.eval (G.cloudList u).length) + (hpe : F.fitLevel hd (2 * G.numEdges) ≤ pol.eval (2 * G.numEdges)) : + preRotFn F pol (F.toFamily hd).degree + (pair (encGraph G) (pair (List.replicate v true) (List.replicate d true))) + = pair (List.replicate (G.preRotNum (F.toFamily hd) v d).1 true) + (List.replicate (G.preRotNum (F.toFamily hd) v d).2 true) := by + have hne : 0 < 2 * G.numEdges := Nat.lt_of_le_of_lt (Nat.zero_le _) hv + have hvd : v / 2 < G.numEdges := by omega + have hulr : G.ownerNum v < G.numVerts := by + rw [ConstraintGraph.ownerNum, dif_pos hvd] + split <;> exact Fin.isLt _ + rw [preRotFn] + simp only [pairFst_pair, pairSnd_pair] + rw [ConstraintGraph.preRotNum] + by_cases h0 : d = 0 + · subst h0 + rw [ifEqLen_pos (by simp), if_pos rfl] + rfl + rw [ifEqLen_neg (by simpa using h0), if_neg h0] + by_cases h1 : d = 1 + · subst h1 + rw [ifEqLen_pos (by simp), if_pos rfl, flipFn_eq] + rfl + rw [ifEqLen_neg (by simpa using h1), if_neg h1] + have hdrop2 : (List.replicate d true).drop 2 = List.replicate (d - 2) true := by simp + by_cases h2 : d < 2 + (F.toFamily hd).degree + · -- the cloud's move + have hjlt : d - 2 < (F.toFamily hd).degree := by omega + rw [ifLtLen_pos (by simpa using h2), if_pos h2, hdrop2, + ownerFn_eq G v hvd, + cloudStepFn_eq G F pol hd ⟨G.ownerNum v, hulr⟩ v (d - 2) hv rfl hjlt + (hpc ⟨G.ownerNum v, hulr⟩)] + rw [ConstraintGraph.cloudStepN, dif_pos hulr, dif_pos hjlt] + simp only [pairFst_pair, pairSnd_pair] + have happ : ∀ n : ℕ, List.replicate n true ++ [true, true] + = List.replicate (n + 2) true := by + intro n + rw [List.replicate_add] + rfl + congr 1 + exact happ _ + · -- the expander's move + have hjlt : d - (2 + (F.toFamily hd).degree) < (F.toFamily hd).degree := by omega + have horder : (G.reduce (F.toFamily hd)).graph.order = 2 * G.numEdges := by + rw [ConstraintGraph.graph_reduce, ConstraintGraph.order_reduceGraph] + have hvlt : v < (G.reduce (F.toFamily hd)).graph.order := by rw [horder]; exact hv + have key : ∀ (n : ℕ) (_ : n = 2 * G.numEdges) (hvn : v < n) + (hjn : d - (2 + (F.toFamily hd).degree) < (F.toFamily hd).degree), + (((F.toFamily hd).rot n (⟨v, hvn⟩, ⟨d - (2 + (F.toFamily hd).degree), hjn⟩)).1.val, + ((F.toFamily hd).rot n + (⟨v, hvn⟩, ⟨d - (2 + (F.toFamily hd).degree), hjn⟩)).2.val) + = F.famRotVal hd (2 * G.numEdges) (v, d - (2 + (F.toFamily hd).degree)) := by + rintro n rfl hvn hjn + exact (F.famRotVal_eq hd hne ⟨v, hvn⟩ ⟨_, hjn⟩).symm + have hdropk : (List.replicate d true).drop (2 + (F.toFamily hd).degree) + = List.replicate (d - (2 + (F.toFamily hd).degree)) true := by simp + rw [ifLtLen_neg (by simpa using h2), if_neg h2, hdropk, + expStepFn_eq G F pol hd v (d - (2 + (F.toFamily hd).degree)) hne hpe] + rw [ConstraintGraph.expStepN, dif_pos hvlt, dif_pos hjlt] + simp only [pairFst_pair, pairSnd_pair] + have hk := key _ horder hvlt hjlt + have hk1 := congrArg Prod.fst hk + have hk2 := congrArg Prod.snd hk + dsimp only at hk1 hk2 ⊢ + rw [hk1, hk2] + congr 1 + rw [Nat.add_assoc, ← List.replicate_add] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgPreprocess.lean b/Complexitylib/Classes/PCP/Internal/AlgPreprocess.lean new file mode 100644 index 00000000..adaac6a8 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgPreprocess.lean @@ -0,0 +1,256 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.Preprocess +public import Complexitylib.Classes.PCP.Internal.CloudCount + +/-! +# The preprocessed graph, in numbers + +Preprocessing superposes three edge sets on the half-edges of a graph: the +edge-links pairing the two halves of an edge, the cloud-links joining the halves +that share an endpoint, and the expander's own edges, with a self-loop added at +every vertex. This module reads off what each of those does to a *number*. + +The numbering is the one `NumEnc` gives: a half-edge is `2 e` or `2 e + 1` +according to which end it is — the same number `DegreeReduction` sorts clouds by +— and a dart is `0` for the self-loop, `1` for the edge-link, `2 + j` for the +`j`-th cloud-link and `2 + degree + j` for the `j`-th expander edge. + +## Main results + +- `Complexity.ConstraintGraph.enc_halfEdge` — a half-edge's number is its code +- `Complexity.ConstraintGraph.enc_preLoop`, `enc_preEdge`, `enc_preCloud`, + `enc_preExp` — the four kinds of dart, and `preDart_cases`: there are no + others +-/ + +@[expose] public section + +namespace Complexity + +namespace ConstraintGraph + +open NumEnc + +variable {α : Type} [DecidableEq α] (G : ConstraintGraph α) (E : ExpanderFamily) + +omit [DecidableEq α] in +/-- **A half-edge's number is the code the clouds are sorted by.** -/ +theorem enc_halfEdge (p : G.HalfEdge) : enc p = G.halfCode p := by + show enc p.1 * 2 + enc p.2 = 2 * p.1.val + (if p.2 then 0 else 1) + show p.1.val * 2 + (if p.2 then 0 else 1) = _ + omega + +/-- The self-loop at a vertex. -/ +def preLoop : (G.preprocess E).graph.D := Sum.inl () + +/-- The link to the other half of the same edge. -/ +def preEdge : (G.preprocess E).graph.D := Sum.inr (Sum.inl none) + +/-- The `j`-th link inside the cloud. -/ +def preCloud (j : Fin E.degree) : (G.preprocess E).graph.D := Sum.inr (Sum.inl (some j)) + +/-- The `j`-th edge of the superposed expander. -/ +def preExp (j : Fin E.degree) : (G.preprocess E).graph.D := Sum.inr (Sum.inr j) + +/-- The darts of the preprocessed graph: a self-loop, an edge-link, the +cloud-links, and the expander's edges. -/ +theorem enc_preLoop : enc (G.preLoop E) = 0 := rfl + +theorem enc_preEdge : enc (G.preEdge E) = 1 := by + show 1 + 0 = 1 + omega + +theorem enc_preCloud (j : Fin E.degree) : enc (G.preCloud E j) = 2 + j.val := by + show 1 + (1 + j.val) = 2 + j.val + omega + +theorem enc_preExp (j : Fin E.degree) : enc (G.preExp E j) = 2 + E.degree + j.val := by + show 1 + ((E.degree + 1) + j.val) = 2 + E.degree + j.val + omega + +/-- Every dart is one of the four kinds. -/ +theorem preDart_cases (d : (G.preprocess E).graph.D) : + d = G.preLoop E ∨ d = G.preEdge E ∨ (∃ j, d = G.preCloud E j) ∨ ∃ j, d = G.preExp E j := by + match d with + | Sum.inl () => exact Or.inl rfl + | Sum.inr (Sum.inl none) => exact Or.inr (Or.inl rfl) + | Sum.inr (Sum.inl (some j)) => exact Or.inr (Or.inr (Or.inl ⟨j, rfl⟩)) + | Sum.inr (Sum.inr j) => exact Or.inr (Or.inr (Or.inr ⟨j, rfl⟩)) + +/-! ### What each kind of dart does -/ + +theorem rot_preLoop (v : (G.preprocess E).graph.V) : + (G.preprocess E).graph.rot (v, G.preLoop E) = (v, G.preLoop E) := rfl + +theorem rot_preEdge (v : G.HalfEdge) : + (G.preprocess E).graph.rot (v, G.preEdge E) = (G.flipHalf v, G.preEdge E) := rfl + +theorem rot_preCloud (v : G.HalfEdge) (j : Fin E.degree) : + (G.preprocess E).graph.rot (v, G.preCloud E j) + = ((G.cloudRot E v j).1, G.preCloud E (G.cloudRot E v j).2) := rfl + +theorem rot_preExp (v : G.HalfEdge) (j : Fin E.degree) : + (G.preprocess E).graph.rot (v, G.preExp E j) + = (E.vertexEquiv (G.reduce E).graph + (E.rot (G.reduce E).graph.order + ((E.vertexEquiv (G.reduce E).graph).symm v, j)).1, + G.preExp E (E.rot (G.reduce E).graph.order + ((E.vertexEquiv (G.reduce E).graph).symm v, j)).2) := rfl + +/-! ### The three moves, in numbers -/ + +omit [DecidableEq α] in +/-- **Crossing an edge flips the last bit of the number.** -/ +theorem enc_flipHalf (p : G.HalfEdge) : + enc (G.flipHalf p) = if enc p % 2 = 0 then enc p + 1 else enc p - 1 := by + rw [enc_halfEdge, enc_halfEdge, halfCode, halfCode, flipHalf] + cases p.2 <;> simp + +/-- **The expander's vertices are numbered as the graph's are.** -/ +theorem enc_vertexEquiv (x : Fin (G.reduce E).graph.order) : + enc (E.vertexEquiv (G.reduce E).graph x) = x.val := by + show enc ((NumEnc.equivFinCard (G.reduce E).graph.V).symm x) = _ + have h : NumEnc.enc ((NumEnc.equivFinCard (G.reduce E).graph.V).symm x) + = (NumEnc.equivFinCard (G.reduce E).graph.V + ((NumEnc.equivFinCard (G.reduce E).graph.V).symm x)).val := rfl + rw [h, Equiv.apply_symm_apply] + +theorem val_vertexEquiv_symm (v : (G.reduce E).graph.V) : + ((E.vertexEquiv (G.reduce E).graph).symm v).val = enc v := rfl + +/-! ### The cloud step, in numbers -/ + +/-- The cloud step on numbers: count how many of the cloud's half-edges come +before this one, let the expander family move that index, and read off the code +the new index names. -/ +noncomputable def cloudStepNum (v : Fin G.numVerts) (c : ℕ) (j : Fin E.degree) : ℕ × ℕ := + if h : countBelow (G.cloudCodes v) c < (G.cloudList v).length then + let q := E.rot (G.cloudList v).length (⟨countBelow (G.cloudCodes v) c, h⟩, j) + ((G.cloudCodes v).orderEmbOfFin (G.card_cloudCodes_eq_length v) q.1, q.2.val) + else (c, j.val) + +omit [DecidableEq α] in +/-- **The numbers run the cloud step.** -/ +theorem cloudStepNum_eq (p : G.HalfEdge) (j : Fin E.degree) : + G.cloudStepNum E (G.owner p) (enc p) j + = (enc (G.cloudRot E p j).1, (G.cloudRot E p j).2.val) := by + have hmem : p ∈ G.cloudList (G.owner p) := G.mem_cloudList_self p + have hlt : (G.cloudList (G.owner p)).idxOf p < (G.cloudList (G.owner p)).length := + List.idxOf_lt_length_iff.mpr hmem + have hidx : (G.cloudList (G.owner p)).idxOf p + = countBelow (G.cloudCodes (G.owner p)) (enc p) := by + rw [G.idxOf_cloudList rfl, enc_halfEdge] + have hlt' : countBelow (G.cloudCodes (G.owner p)) (enc p) + < (G.cloudList (G.owner p)).length := by rw [← hidx]; exact hlt + rw [cloudStepNum, dif_pos hlt', cloudRot, cloudRotAux, dif_pos hlt] + simp only [← hidx] + refine Prod.ext ?_ rfl + dsimp only + conv_rhs => rw [enc_halfEdge] + have hq : (E.rot (G.cloudList (G.owner p)).length + (⟨(G.cloudList (G.owner p)).idxOf p, hlt⟩, j)).1.val + < (G.cloudList (G.owner p)).length := Fin.isLt _ + rw [← List.getElem_eq_getD (h := hq)] + exact G.halfCode_getElem_cloudList _ _ hq + +/-! ### The whole rotation map, in numbers -/ + +/-- The vertex a half-edge number is attached to. -/ +noncomputable def ownerNum (v : ℕ) : ℕ := + if h : v / 2 < G.numEdges then + (if v % 2 = 0 then (G.head ⟨v / 2, h⟩).val else (G.tail ⟨v / 2, h⟩).val) + else 0 + +omit [DecidableEq α] in +theorem ownerNum_enc (p : G.HalfEdge) : G.ownerNum (enc p) = (G.owner p).val := by + have hcode : enc p = 2 * p.1.val + (if p.2 then 0 else 1) := by + rw [enc_halfEdge, halfCode] + have hdiv : enc p / 2 = p.1.val := by + rw [hcode] + cases p.2 + · simp + omega + · simp + have hmod : enc p % 2 = (if p.2 then 0 else 1) := by + rw [hcode] + cases p.2 <;> simp + have hlt : enc p / 2 < G.numEdges := by rw [hdiv]; exact p.1.isLt + have hfin : (⟨enc p / 2, hlt⟩ : Fin G.numEdges) = p.1 := Fin.ext hdiv + rw [ownerNum, dif_pos hlt, hmod, owner, hfin] + cases p.2 <;> simp + +/-- The cloud step, on numbers throughout. -/ +noncomputable def cloudStepN (u c j : ℕ) : ℕ × ℕ := + if hu : u < G.numVerts then + if hj : j < E.degree then G.cloudStepNum E ⟨u, hu⟩ c ⟨j, hj⟩ else (c, j) + else (c, j) + +/-- The expander step, on numbers. -/ +noncomputable def expStepN (v j : ℕ) : ℕ × ℕ := + if hv : v < (G.reduce E).graph.order then + if hj : j < E.degree then + ((E.rot (G.reduce E).graph.order (⟨v, hv⟩, ⟨j, hj⟩)).1.val, + (E.rot (G.reduce E).graph.order (⟨v, hv⟩, ⟨j, hj⟩)).2.val) + else (v, j) + else (v, j) + +/-- **The preprocessed graph's rotation map, on numbers.** Dart `0` is the +self-loop, dart `1` crosses the edge, darts `2` to `deg + 1` rotate inside the +cloud, and the rest are the superposed expander's. -/ +noncomputable def preRotNum (v d : ℕ) : ℕ × ℕ := + if d = 0 then (v, 0) + else if d = 1 then ((if v % 2 = 0 then v + 1 else v - 1), 1) + else if d < 2 + E.degree then + ((G.cloudStepN E (G.ownerNum v) v (d - 2)).1, + (G.cloudStepN E (G.ownerNum v) v (d - 2)).2 + 2) + else + ((G.expStepN E v (d - (2 + E.degree))).1, + (G.expStepN E v (d - (2 + E.degree))).2 + 2 + E.degree) + +/-- **The numbers run the preprocessed graph's rotation map.** -/ +theorem preRotNum_eq (v : G.HalfEdge) (d : (G.preprocess E).graph.D) : + G.preRotNum E (enc v) (enc d) + = (enc (((G.preprocess E).graph.rot (v, d)).1 : G.HalfEdge), + enc ((G.preprocess E).graph.rot (v, d)).2) := by + rcases G.preDart_cases E d with rfl | rfl | ⟨j, rfl⟩ | ⟨j, rfl⟩ + · rw [G.enc_preLoop E, preRotNum, if_pos rfl, G.rot_preLoop E, G.enc_preLoop E] + rfl + · rw [G.enc_preEdge E, preRotNum, if_neg one_ne_zero, if_pos rfl, G.rot_preEdge E, + G.enc_preEdge E] + dsimp only + exact Prod.ext (G.enc_flipHalf v).symm rfl + · have hj := j.isLt + rw [G.enc_preCloud E j, preRotNum, if_neg (by omega), if_neg (by omega), + if_pos (by omega), G.rot_preCloud E, G.enc_preCloud E] + dsimp only + rw [G.ownerNum_enc] + have harg : (2 + j.val) - 2 = j.val := by omega + rw [harg, cloudStepN, dif_pos (G.owner v).isLt, dif_pos hj] + have hv : (⟨(G.owner v).val, (G.owner v).isLt⟩ : Fin G.numVerts) = G.owner v := rfl + have hjj : (⟨j.val, hj⟩ : Fin E.degree) = j := rfl + rw [hv, hjj, G.cloudStepNum_eq E v j] + exact Prod.ext rfl (by omega) + · have hj := j.isLt + have hvlt : enc v < (G.reduce E).graph.order := by + have := NumEnc.enc_lt v + rwa [NumEnc.card_eq_fintype_card] at this + rw [G.enc_preExp E j, preRotNum, if_neg (by omega), if_neg (by omega), + if_neg (by omega), G.rot_preExp E, G.enc_preExp E] + dsimp only + have harg : 2 + E.degree + j.val - (2 + E.degree) = j.val := by omega + rw [harg, expStepN, dif_pos hvlt, dif_pos hj] + have hv : (⟨enc v, hvlt⟩ : Fin (G.reduce E).graph.order) + = (E.vertexEquiv (G.reduce E).graph).symm v := + Fin.ext (G.val_vertexEquiv_symm E v).symm + have hjj : (⟨j.val, hj⟩ : Fin E.degree) = j := rfl + rw [hv, hjj] + exact Prod.ext (G.enc_vertexEquiv E _).symm (by omega) + +end ConstraintGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgRound.lean b/Complexitylib/Classes/PCP/Internal/AlgRound.lean new file mode 100644 index 00000000..73f0c758 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgRound.lean @@ -0,0 +1,174 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.AlgEdge +public import Complexitylib.Classes.PCP.Internal.AlgIter +public import Complexitylib.Classes.PCP.Internal.GapReduction + +/-! +# One round, uniformly + +`AlgEdge.stepFn_eq` computes a round of amplification for a graph whose sizes +match the constants it is given. Iterating a round needs more: *one* function +that is right for *every* graph. This module supplies the constants that do not +depend on the graph — the walk length, the degree, the tester's counts — and +specialises the round to them. + +The one fact that makes this possible is that the encoding a round composes with +does not depend on the graph either: the walks it is defined on are +`PreWalk E T`, whose type is fixed by the expander family alone. + +## Main definitions + +- `Complexity.roundOf` — the round's constants +- `Complexity.roundFn` — the round, as one `FP` function + +## Main results + +- `Complexity.roundFn_eq` — it computes a round of amplification, for every + graph +-/ + +@[expose] public section + +set_option maxRecDepth 8000 + +namespace Complexity + +open Dinur Tester BooleanAnalysis + +variable (F : FinBase) (hd : 1 < F.deg) + +/-! ### The constants -/ + +/-- The killing rate a round uses. -/ +noncomputable def qOf : ℕ := q₀ (F.toFamily hd) + +theorem qOf_pos : 0 < qOf F hd := by + have h := two_le_q₀ (F.toFamily hd) + show 0 < q₀ (F.toFamily hd) + omega + +/-- The walk length a round uses. -/ +noncomputable def walkLen : ℕ := powT K (qOf F hd) + +/-- How many random strings the tester has. -/ +noncomputable def cZOf : ℕ := 2 ^ ROf (bits (F.toFamily hd) (walkLen F hd)) + +/-- How many constraints the alphabet has. -/ +noncomputable def cRel : ℕ := Fintype.card (DinurAlpha → DinurAlpha → Bool) + +theorem cRel_eq : cRel = Fintype.card (DinurAlpha → DinurAlpha → Bool) := rfl + +theorem cRel_pos : 0 < cRel := Fintype.card_pos + +/-- The round's constants. -/ +noncomputable def roundOf : Round := + dinurRound F hd (qOf F hd) cRel (cZOf F hd) + +theorem roundOf_q : (roundOf F hd).q = qOf F hd := + dinurRound_q F hd (qOf F hd) cRel (cZOf F hd) + +theorem roundOf_T : (roundOf F hd).T = walkLen F hd := + dinurRound_T F hd (qOf F hd) cRel (cZOf F hd) + +theorem roundOf_C : (roundOf F hd).C = cRel := + dinurRound_C F hd (qOf F hd) cRel (cZOf F hd) + +theorem roundOf_cZ : (roundOf F hd).cZ = cZOf F hd := + dinurRound_cZ F hd (qOf F hd) cRel (cZOf F hd) + +theorem roundOf_deg : (roundOf F hd).deg = (F.toFamily hd).degree := + dinurRound_deg F hd (qOf F hd) cRel (cZOf F hd) + +theorem roundOf_P (G : ConstraintGraph DinurAlpha) : + (roundOf F hd).P = G.preDeg (F.toFamily hd) := + dinurRound_P F hd (qOf F hd) cRel (cZOf F hd) G + +theorem roundOf_cQ : (roundOf F hd).cQ = qOf F hd ^ walkLen F hd := + dinurRound_cQ F hd (qOf F hd) cRel (cZOf F hd) + +/-- A key to fall back on, for arguments that name no edge. -/ +noncomputable def dfltKey : StepKey (F.toFamily hd) (roundOf F hd).T (roundOf F hd).q + (bits (F.toFamily hd) (roundOf F hd).T) + (Fintype.card (DinurAlpha → DinurAlpha → Bool)) := + ⟨⟨fun _ => Sum.inl (), fun _ => ⟨0, qOf_pos F hd⟩⟩, + ⟨fun _ => 0, fun _ => ⟨0, Fintype.card_pos⟩⟩, + ⟨fun _ => Sum.inl (), ⟨0, ReadIdx.f1x⟩⟩⟩ + +/-- The encoding a round composes with. It is written at a graph, but does not +depend on it: the walks are those of the expander family. -/ +noncomputable def encOf : (PreWalk (F.toFamily hd) (roundOf F hd).T → DinurAlpha) → + Cube (bits (F.toFamily hd) (roundOf F hd).T) := + Dinur.enc (F.toFamily hd) (baseCSP []) (roundOf F hd).T + +/-! ### The round -/ + +/-- **A round of amplification, as one function.** -/ +noncomputable def roundFn : List Bool → List Bool := + stepFn F (2 * Polynomial.X) (roundOf F hd) + (vertFactor (F.toFamily hd) (qOf F hd)) + (edgeFactor (F.toFamily hd) (qOf F hd)) + (posFactor (F.toFamily hd) (qOf F hd)) + (NumEnc.card (Cube (bits (F.toFamily hd) (roundOf F hd).T))) + (NumEnc.card (Cube (nOf (bits (F.toFamily hd) (roundOf F hd).T)))) + (NumEnc.card (Cube (nOf (bits (F.toFamily hd) (roundOf F hd).T) + * nOf (bits (F.toFamily hd) (roundOf F hd).T)))) + (dfltKey F hd) (encOf F hd) + +theorem roundOf_cQ_pos : 0 < (roundOf F hd).cQ := by + rw [roundOf_cQ] + exact Nat.pow_pos (qOf_pos F hd) + +theorem roundOf_cD_pos : 0 < (roundOf F hd).cD := by + rw [Round.cD, roundOf_q, roundOf_P F hd (baseCSP []), roundOf_T] + exact Nat.mul_pos (Nat.pow_pos ((baseCSP []).preDeg_pos (F.toFamily hd))) + (Nat.pow_pos (qOf_pos F hd)) + +theorem roundOf_cZ_pos : 0 < (roundOf F hd).cZ := by + rw [roundOf_cZ, cZOf] + exact Nat.two_pow_pos _ + +theorem roundOf_C_pos : 0 < (roundOf F hd).C := by + rw [roundOf_C] + exact cRel_pos + +theorem roundFn_mem_FP : roundFn F hd ∈ FP := + stepFn_mem_FP F (2 * Polynomial.X) (roundOf F hd) _ _ _ _ _ _ + (roundOf_cQ_pos F hd) (roundOf_cD_pos F hd) (roundOf_cZ_pos F hd) (roundOf_C_pos F hd) + (dfltKey F hd) (encOf F hd) + +/-- **The round computes a round of amplification, for every graph.** -/ +theorem roundFn_eq (G : ConstraintGraph DinurAlpha) : + roundFn F hd (encGraph G) + = encGraph (Dinur.step (F.toFamily hd) (qOf F hd) (qOf_pos F hd) G) := by + have hq : 0 < (roundOf F hd).q := by + rw [roundOf_q] + exact qOf_pos F hd + have hpol : ∀ n : ℕ, F.fitLevel hd n ≤ (2 * Polynomial.X : Polynomial ℕ).eval n := by + intro n + simpa using F.fitLevel_le hd n + have hrD : (roundOf F hd).cD + = NumEnc.card ((G.preprocess (F.toFamily hd)).killedPow + (roundOf F hd).q (roundOf F hd).T hq).graph.D := by + rw [NumEnc.card_eq_fintype_card, RegCSP.graph_killedPow] + show _ = ((G.preprocess (F.toFamily hd)).graph.killedPower + (roundOf F hd).q (roundOf F hd).T hq).deg + rw [RegGraph.deg_killedPower, G.deg_preprocess, Round.cD, roundOf_P F hd G, + G.preDeg_eq (F.toFamily hd)] + have hrZ : (roundOf F hd).cZ + = 2 ^ ROf (bits (F.toFamily hd) (roundOf F hd).T) := by + rw [roundOf_cZ, roundOf_T, cZOf] + refine stepFn_eq F (2 * Polynomial.X) hd G (roundOf F hd) hq _ _ _ _ _ _ + hrD hrZ (roundOf_deg F hd) (roundOf_P F hd G) ((roundOf_C F hd).trans cRel_eq) + ?_ ?_ ?_ ?_ rfl rfl rfl ?_ (dfltKey F hd) + · exact fun u => hpol _ + · exact hpol _ + · exact Dinur.numVerts_step (F.toFamily hd) _ hq G + · exact Dinur.numEdges_step (F.toFamily hd) _ hq G + · exact (Dinur.card_pos_step (F.toFamily hd) _ hq G).trans (Nat.mul_comm _ _) + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgStep.lean b/Complexitylib/Classes/PCP/Internal/AlgStep.lean new file mode 100644 index 00000000..ec174325 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgStep.lean @@ -0,0 +1,251 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.Dinur +public import Complexitylib.Classes.PCP.Internal.AlgCompose + +/-! +# One round, in numbers + +A round of amplification multiplies both counts of a constraint graph by a +constant factor. This module records those factors, so an algorithm that writes +the round's output knows how big it is. + +## Main results + +- `Complexity.MultiTest.numVerts_toGraph` — the vertices of a family's graph +- `Complexity.RegCSP.card_pos_compose` — the positions of a composed proof +- `Complexity.Dinur.numVerts_step` — a round's vertex count +- `Complexity.MultiTest.tailNum_eq` — the first endpoint of an edge +- `Complexity.MultiTest.rel_toGraph_eq` — its constraint, from the verdict and + the read +-/ + +@[expose] public section + +namespace Complexity + +open BooleanAnalysis + +namespace MultiTest + +variable {Pos E Q : Type} (M : MultiTest Pos E Q) [Fintype Pos] [Fintype E] [Fintype Q] + [NumEnc Pos] [NumEnc E] [NumEnc Q] + +/-- **The vertices**: one per position, and one per (test, random string). -/ +theorem numVerts_toGraph : + M.toGraph.numVerts = Fintype.card Pos + Fintype.card E * 2 ^ M.R := by + show Fintype.card (Pos ⊕ (E × Cube M.R)) = _ + rw [Fintype.card_sum, Fintype.card_prod, card_cube] + +/-- Splitting a number into a quotient and a remainder, with the remainder +itself split. -/ +theorem split_mixed {a b c C Q : ℕ} (hb : b < C) (hc : c < Q) : + (a * (C * Q) + (b * Q + c)) / (C * Q) = a + ∧ (a * (C * Q) + (b * Q + c)) % (C * Q) / Q = b + ∧ (a * (C * Q) + (b * Q + c)) % Q = c := by + have hQ : 0 < Q := Nat.lt_of_le_of_lt (Nat.zero_le _) hc + have hCQ : 0 < C * Q := Nat.mul_pos (Nat.lt_of_le_of_lt (Nat.zero_le _) hb) hQ + have hlt : b * Q + c < C * Q := by + have : b * Q + Q ≤ C * Q := by + have : (b + 1) * Q ≤ C * Q := Nat.mul_le_mul_right _ hb + rw [Nat.add_mul, Nat.one_mul] at this + exact this + omega + have hmod : (a * (C * Q) + (b * Q + c)) % (C * Q) = b * Q + c := by + rw [Nat.mul_comm a (C * Q), Nat.mul_add_mod, Nat.mod_eq_of_lt hlt] + refine ⟨?_, ?_, ?_⟩ + · rw [Nat.mul_comm a (C * Q), Nat.mul_add_div hCQ, Nat.div_eq_of_lt hlt, Nat.add_zero] + · rw [hmod, Nat.mul_comm b Q, Nat.mul_add_div hQ, Nat.div_eq_of_lt hc, Nat.add_zero] + · have hre : a * (C * Q) + (b * Q + c) = c + (a * C + b) * Q := by ring + rw [hre, Nat.add_mul_mod_self_right, Nat.mod_eq_of_lt hc] + +omit [Fintype Pos] [NumEnc Pos] in +/-- **How an edge number splits**: the test, the random string, the read. -/ +theorem enc_edgeOf_split (k : Fin (Fintype.card M.Edge)) : + NumEnc.enc (M.edgeOf k).1 = k.val / (2 ^ M.R * Fintype.card Q) + ∧ NumEnc.enc (M.edgeOf k).2.1 = k.val % (2 ^ M.R * Fintype.card Q) / Fintype.card Q + ∧ NumEnc.enc (M.edgeOf k).2.2 = k.val % Fintype.card Q := by + have hk : k.val = NumEnc.enc (M.edgeOf k).1 * (2 ^ M.R * Fintype.card Q) + + (NumEnc.enc (M.edgeOf k).2.1 * Fintype.card Q + NumEnc.enc (M.edgeOf k).2.2) := by + rw [← M.enc_edgeOf k, M.enc_edge] + rw [show NumEnc.card (Cube M.R) = 2 ^ M.R from by + rw [NumEnc.card_eq_fintype_card, card_cube]] + rw [show NumEnc.card Q = Fintype.card Q from NumEnc.card_eq_fintype_card Q] + have hb : NumEnc.enc (M.edgeOf k).2.1 < 2 ^ M.R := by + have := NumEnc.enc_lt (M.edgeOf k).2.1 + rwa [NumEnc.card_eq_fintype_card, card_cube] at this + have hc : NumEnc.enc (M.edgeOf k).2.2 < Fintype.card Q := by + have := NumEnc.enc_lt (M.edgeOf k).2.2 + rwa [NumEnc.card_eq_fintype_card] at this + obtain ⟨h1, h2, h3⟩ := split_mixed hb hc + exact ⟨by rw [hk, h1], by rw [hk, h2], by rw [hk, h3]⟩ + +/-- **The first endpoint of an edge, in numbers.** -/ +noncomputable def tailNum (k : ℕ) : ℕ := + Fintype.card Pos + + (k / (2 ^ M.R * Fintype.card Q) * 2 ^ M.R + + k % (2 ^ M.R * Fintype.card Q) / Fintype.card Q) + +theorem tailNum_eq (k : Fin (Fintype.card M.Edge)) : + M.tailNum k.val = (M.toGraph.tail k).val := by + obtain ⟨h1, h2, _⟩ := M.enc_edgeOf_split k + rw [tailNum, M.val_tail_toGraph, ← h1, ← h2, NumEnc.card_eq_fintype_card Pos, + NumEnc.card_eq_fintype_card (Cube M.R), card_cube] + +/-- The constraint an edge carries: the test vertex's answers must pass the +test, and the read's answer must be the position's bit. -/ +def relOfCheck (chk : (Q → ZMod 2) → Bool) (i : Q) : + Alpha Q → Alpha Q → Bool := + fun l₁ l₂ => decide (chk l₁.2 = true ∧ l₁.2 i = l₂.1) + +/-- **The constraint depends only on the verdict and the read.** -/ +theorem rel_toGraph_eq (k : Fin (Fintype.card M.Edge)) : + M.toGraph.rel k + = relOfCheck (M.check (M.edgeOf k).1 (M.edgeOf k).2.1) (M.edgeOf k).2.2 := rfl + +end MultiTest + +namespace RegCSP + +variable {β : Type} [Fintype β] [DecidableEq β] [Nonempty β] (R : RegCSP β) + [NumEnc R.graph.V] [NumEnc R.graph.D] (B : ℕ) + +omit [Fintype β] [DecidableEq β] [Nonempty β] [NumEnc R.graph.V] [NumEnc R.graph.D] in +/-- **The positions of a composed proof**: an encoding block per vertex, and a +linear and a quadratic table per dart. -/ +theorem card_pos_compose : + Fintype.card (R.Pos (B := B)) + = R.graph.order * 2 ^ B + + (R.graph.order * R.graph.deg * 2 ^ Tester.nOf B + + R.graph.order * R.graph.deg * 2 ^ (Tester.nOf B * Tester.nOf B)) := by + show Fintype.card ((R.graph.V × Cube B) ⊕ + ((R.Dart × Cube (Tester.nOf B)) ⊕ (R.Dart × Cube (Tester.nOf B * Tester.nOf B)))) = _ + rw [Fintype.card_sum, Fintype.card_sum, + show Fintype.card (R.graph.V × Cube B) = R.graph.order * 2 ^ B from by + rw [Fintype.card_prod, card_cube]; rfl, + show Fintype.card (R.Dart × Cube (Tester.nOf B)) + = R.graph.order * R.graph.deg * 2 ^ Tester.nOf B from by + rw [Fintype.card_prod, card_cube, R.card_dart], + show Fintype.card (R.Dart × Cube (Tester.nOf B * Tester.nOf B)) + = R.graph.order * R.graph.deg * 2 ^ (Tester.nOf B * Tester.nOf B) from by + rw [Fintype.card_prod, card_cube, R.card_dart]] + +omit [DecidableEq β] [Nonempty β] [NumEnc R.graph.V] [NumEnc R.graph.D] in +/-- The composed test's randomness. -/ +theorem R_compose (enc : β → Cube B) : (R.compose enc).R = Tester.ROf B := rfl + +omit [DecidableEq β] [Nonempty β] in +/-- **An edge number splits into a vertex, a dart, a string and a read.** -/ +theorem edge_split (enc : β → Cube B) (k : Fin (Fintype.card (R.compose enc).Edge)) : + k.val = ((NumEnc.enc ((R.compose enc).edgeOf k).1.1 * NumEnc.card R.graph.D + + NumEnc.enc ((R.compose enc).edgeOf k).1.2) + * 2 ^ Tester.ROf B + + NumEnc.enc ((R.compose enc).edgeOf k).2.1) * 22 + + NumEnc.enc ((R.compose enc).edgeOf k).2.2 := by + have h1 := MultiTest.enc_edgeOf (M := R.compose enc) k + have h2 := MultiTest.enc_edge (M := R.compose enc) ((R.compose enc).edgeOf k) + have h3 := RegCSP.enc_dart R ((R.compose enc).edgeOf k).1 + have hq : NumEnc.card ReadIdx = 22 := rfl + have hcube : NumEnc.card (Cube (R.compose enc).R) = 2 ^ Tester.ROf B := by + rw [NumEnc.card_eq_fintype_card, card_cube, R_compose] + rw [h2, h3, hq, hcube] at h1 + rw [← h1] + ring + +omit [DecidableEq β] [Nonempty β] [NumEnc R.graph.V] [NumEnc R.graph.D] in +/-- **The first endpoint, from an edge's split.** -/ +theorem tailNum_split (enc : β → Cube B) (a c d : ℕ) (hc : c < 2 ^ Tester.ROf B) + (hd : d < 22) : + (R.compose enc).tailNum ((a * 2 ^ Tester.ROf B + c) * 22 + d) + = Fintype.card (R.Pos (B := B)) + (a * 2 ^ Tester.ROf B + c) := by + have hQ : Fintype.card ReadIdx = 22 := rfl + have hR : (2 : ℕ) ^ (R.compose enc).R = 2 ^ Tester.ROf B := rfl + have hre : (a * 2 ^ Tester.ROf B + c) * 22 + d + = a * (2 ^ Tester.ROf B * 22) + (c * 22 + d) := by ring + obtain ⟨h1, h2, _⟩ := MultiTest.split_mixed (a := a) hc hd + rw [MultiTest.tailNum, hQ, hR, hre, h1, h2] + +omit [DecidableEq β] [Nonempty β] [NumEnc R.graph.V] [NumEnc R.graph.D] in +/-- **The first endpoint**, with the string count given by name. -/ +theorem tailNum_split' (enc : β → Cube B) (cZ a c d : ℕ) (hcZ : cZ = 2 ^ Tester.ROf B) + (hc : c < cZ) (hd : d < 22) : + (R.compose enc).tailNum ((a * cZ + c) * 22 + d) + = Fintype.card (R.Pos (B := B)) + (a * cZ + c) := by + subst hcZ + exact tailNum_split R B enc a c d hc hd + +omit [DecidableEq β] [Nonempty β] in +/-- **An edge's data**, packaged so that a caller never has to spell the +composed system out: the test, the string and the read it names, together with +how its number splits. -/ +theorem edge_data (enc : β → Cube B) (e : ℕ) + (he : e < (R.compose enc).toGraph.numEdges) : + ∃ (p : R.Dart) (z : Cube (Tester.ROf B)) (i : ReadIdx), + p = ((R.compose enc).edgeOf ⟨e, he⟩).1 + ∧ z = ((R.compose enc).edgeOf ⟨e, he⟩).2.1 + ∧ i = ((R.compose enc).edgeOf ⟨e, he⟩).2.2 + ∧ e = ((NumEnc.enc p.1 * NumEnc.card R.graph.D + NumEnc.enc p.2) + * 2 ^ Tester.ROf B + NumEnc.enc z) * 22 + NumEnc.enc i := by + refine ⟨((R.compose enc).edgeOf ⟨e, he⟩).1, ((R.compose enc).edgeOf ⟨e, he⟩).2.1, + ((R.compose enc).edgeOf ⟨e, he⟩).2.2, rfl, rfl, rfl, ?_⟩ + exact edge_split R B enc ⟨e, he⟩ + +end RegCSP + +namespace Dinur + +variable (E : ExpanderFamily) + +/-- The constant factor by which a round multiplies the vertex count. -/ +noncomputable def vertFactor (q : ℕ) : ℕ := + 2 * (2 ^ bits E (powT K q) + + powDeg E ^ powT K q * q ^ powT K q + * (2 ^ Tester.nOf (bits E (powT K q)) + + 2 ^ (Tester.nOf (bits E (powT K q)) * Tester.nOf (bits E (powT K q))) + + 2 ^ Tester.ROf (bits E (powT K q)))) + +/-- The constant number of positions a round makes per edge of its input. -/ +noncomputable def posFactor (q : ℕ) : ℕ := + 2 * (2 ^ bits E (powT K q) + + powDeg E ^ powT K q * q ^ powT K q + * (2 ^ Tester.nOf (bits E (powT K q)) + + 2 ^ (Tester.nOf (bits E (powT K q)) * Tester.nOf (bits E (powT K q))))) + +/-- **The positions of a round's proof, counted.** -/ +theorem card_pos_step (q : ℕ) (hq : 0 < q) (G : ConstraintGraph DinurAlpha) : + Fintype.card (((G.preprocess E).killedPow q (powT K q) hq).Pos + (B := bits E (powT K q))) + = posFactor E q * G.numEdges := by + have horder : ((G.preprocess E).killedPow q (powT K q) hq).graph.order = 2 * G.numEdges := by + rw [RegCSP.graph_killedPow, RegGraph.order_killedPower, G.order_preprocess] + have hdeg : ((G.preprocess E).killedPow q (powT K q) hq).graph.deg + = powDeg E ^ powT K q * q ^ powT K q := by + rw [RegCSP.graph_killedPow, RegGraph.deg_killedPower, G.deg_preprocess, powDeg] + rw [RegCSP.card_pos_compose, horder, hdeg, posFactor] + ring + +/-- **A round multiplies the vertex count by a constant.** -/ +theorem numVerts_step (q : ℕ) (hq : 0 < q) (G : ConstraintGraph DinurAlpha) : + (step E q hq G).numVerts = vertFactor E q * G.numEdges := by + have horder : ((G.preprocess E).killedPow q (powT K q) hq).graph.order = 2 * G.numEdges := by + rw [RegCSP.graph_killedPow, RegGraph.order_killedPower, G.order_preprocess] + have hdeg : ((G.preprocess E).killedPow q (powT K q) hq).graph.deg + = powDeg E ^ powT K q * q ^ powT K q := by + rw [RegCSP.graph_killedPow, RegGraph.deg_killedPower, G.deg_preprocess, powDeg] + have hdart : Fintype.card ((G.preprocess E).killedPow q (powT K q) hq).Dart + = 2 * G.numEdges * (powDeg E ^ powT K q * q ^ powT K q) := by + rw [RegCSP.card_dart, horder, hdeg] + have hR : (RegCSP.compose (enc E G (powT K q)) + ((G.preprocess E).killedPow q (powT K q) hq)).R + = Tester.ROf (bits E (powT K q)) := rfl + rw [step, MultiTest.numVerts_toGraph, RegCSP.card_pos_compose, horder, hdeg, hdart, hR, + vertFactor] + ring + +end Dinur + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgUniform.lean b/Complexitylib/Classes/PCP/Internal/AlgUniform.lean new file mode 100644 index 00000000..558c3b8a --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgUniform.lean @@ -0,0 +1,118 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.NormalForm +public import Complexitylib.Classes.PCP.Internal.UnaryList +public import Complexitylib.SAT.Encoding + +/-! +# A size that only the length decides + +A `PCP` verifier's coin count is a function of the input's *length*. The graph +it reads therefore has to have a size the length alone decides, which the graph +of a formula does not. The remedy is to pad every graph up to one common size — +and for that one needs a size that is both computable and large enough for every +input of that length. + +Any `FP` function has one: a machine that runs in time `p` writes at most +`p |x|` bits, so `p` bounds the output length uniformly over inputs of a given +length, and `Cobham.exists_exact_ruler` writes `p |x|` marks. + +## Main results + +- `Complexity.exists_length_bound` — an `FP` function's output is polynomially + long +- `Complexity.exists_padRuler` — hence a uniform, computable padding size +-/ + +@[expose] public section + +namespace Complexity + +/-- **An `FP` function's output is polynomially long.** -/ +theorem exists_length_bound {f : List Bool → List Bool} (hf : f ∈ FP) : + ∃ p : Polynomial ℕ, ∀ x, (f x).length ≤ p.eval x.length := by + rw [mem_FP_iff_computesInTime_polynomial] at hf + obtain ⟨_, _, p, hcomp⟩ := hf + exact ⟨p, fun x => hcomp.output_length_le x⟩ + +/-- A formula is no longer than its encoding. -/ +theorem length_le_length_encode (φ : SAT.CNF) : φ.length ≤ φ.encode.length := by + induction φ with + | nil => simp + | cons c cs ih => + rw [SAT.CNF.encode_cons, List.length_cons, List.length_append, List.length_append] + simp only [List.length_cons, List.length_nil] + omega + +/-- **A uniform padding size**: marks, as many as any input of that length can +force, and as many for one input as for any other of the same length. -/ +theorem exists_padRuler {f : List Bool → List Bool} (hf : f ∈ FP) (c : ℕ) : + ∃ (padU : List Bool → List Bool) (q : Polynomial ℕ), padU ∈ FP + ∧ (∀ x, padU x = List.replicate (padU x).length true) + ∧ (∀ x, (padU x).length = q.eval x.length) + ∧ ∀ x, c * (f x).length ≤ (padU x).length := by + obtain ⟨p, hp⟩ := exists_length_bound hf + obtain ⟨R, hR, hRlen⟩ := Cobham.exists_exact_ruler (Polynomial.C c * p) + refine ⟨fun z => marks (R z), Polynomial.C c * p, marks_mem_FP hR, fun x => ?_, ?_, ?_⟩ + · show marks (R x) = List.replicate (marks (R x)).length true + rw [marks_eq, List.length_replicate] + · intro x + show (marks (R x)).length = _ + rw [marks_eq, List.length_replicate, hRlen] + · intro x + show c * (f x).length ≤ (marks (R x)).length + rw [marks_eq, List.length_replicate, hRlen, Polynomial.eval_mul, Polynomial.eval_C] + exact Nat.mul_le_mul_left c (hp x) + +/-! ### Rulers, structurally + +Writing a polynomial out and evaluating it is unworkable here: the constants +involved are the alphabet's constraint count and the round's growth factor, and +no tactic may be allowed near them. So the width is built from closure +properties instead — a sum of rulers is an append, a product is a length +multiplication — and no arithmetic is ever performed on a constant. -/ + +/-- A function of the input's length that an `FP` string is long enough for. -/ +def HasRuler (f : ℕ → ℕ) : Prop := + ∃ R : List Bool → List Bool, R ∈ FP ∧ ∀ z : List Bool, f z.length ≤ (R z).length + +namespace HasRuler + +theorem of_poly (p : Polynomial ℕ) : HasRuler fun n => p.eval n := Cobham.exists_ruler p + +theorem const (c : ℕ) : HasRuler fun _ => c := + ⟨fun _ => List.replicate c false, constFn_mem_FP _, fun _ => by rw [List.length_replicate]⟩ + +theorem mono {f g : ℕ → ℕ} (hg : HasRuler g) (h : ∀ n, f n ≤ g n) : HasRuler f := by + obtain ⟨R, hR, hlen⟩ := hg + exact ⟨R, hR, fun z => le_trans (h z.length) (hlen z)⟩ + +theorem add {f g : ℕ → ℕ} (hf : HasRuler f) (hg : HasRuler g) : + HasRuler fun n => f n + g n := by + obtain ⟨R, hR, hRlen⟩ := hf + obtain ⟨S, hS, hSlen⟩ := hg + refine ⟨fun z => R z ++ S z, Cobham.appendFn_mem_FP hR hS, fun z => ?_⟩ + rw [List.length_append] + exact Nat.add_le_add (hRlen z) (hSlen z) + +theorem mul {f g : ℕ → ℕ} (hf : HasRuler f) (hg : HasRuler g) : + HasRuler fun n => f n * g n := by + obtain ⟨R, hR, hRlen⟩ := hf + obtain ⟨S, hS, hSlen⟩ := hg + refine ⟨fun z => List.replicate ((R z).length * (S z).length) false, + Cobham.mulLenFn_mem_FP hR hS, fun z => ?_⟩ + rw [List.length_replicate] + exact Nat.mul_le_mul (hRlen z) (hSlen z) + +theorem pow {f : ℕ → ℕ} (hf : HasRuler f) (d : ℕ) : HasRuler fun n => f n ^ d := by + induction d with + | zero => exact mono (const 1) fun n => by rw [pow_zero] + | succ d ih => exact mono (mul ih hf) fun n => by rw [pow_succ] + +end HasRuler + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlgWalk.lean b/Complexitylib/Classes/PCP/Internal/AlgWalk.lean new file mode 100644 index 00000000..adeb6c73 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlgWalk.lean @@ -0,0 +1,208 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.KilledCSP +public import Complexitylib.Classes.PCP.Internal.NumEncPi +public import Complexitylib.Classes.PCP.Internal.AlgPreprocess + +/-! +# The killed walk, in numbers + +A dart of the powered graph is a tuple of steps together with a tuple of coins; +the walk runs until the first coin that is zero. `NumEncPi` numbers both tuples +digit by digit, so an algorithm recovers a step or a coin by dividing and taking +the remainder. This module reads the stopping rule off those digits. + +## Main definitions + +- `Complexity.stopAtNum` — where a killed walk stops, from the coins' number + +## Main results + +- `Complexity.stopAtNum_eq` — it is the abstract stopping index +- `Complexity.ConstraintGraph.walkNum_eq` — and following the step digits walks + the preprocessed graph +- `Complexity.ConstraintGraph.killedRevNum_eq` — the dart a killed walk comes + back by, digit by digit +- `Complexity.ConstraintGraph.killedRotNum_eq` — the powered graph's rotation + map, in numbers +-/ + +@[expose] public section + +namespace Complexity + +open NumEnc + +theorem findIdx_congr {β : Type} {l : List β} {p q : β → Bool} (h : ∀ x ∈ l, p x = q x) : + l.findIdx p = l.findIdx q := by + induction l with + | nil => rfl + | cons a t ih => + rw [List.findIdx_cons, List.findIdx_cons, h a List.mem_cons_self, + ih fun x hx => h x (List.mem_cons_of_mem _ hx)] + +/-- Where a killed walk stops, read off the coins' number: the first digit that +is zero, or the whole length if there is none. -/ +def stopAtNum (T q c : ℕ) : ℕ := + (List.finRange T).findIdx fun j => (c / q ^ j.val) % q == 0 + +/-- **The digits give the stopping index.** -/ +theorem stopAtNum_eq {T q : ℕ} (hq : 0 < q) (c : Fin T → Fin q) : + stopAtNum T q (enc c) = stopAt c := by + rw [stopAtNum, stopAt] + refine findIdx_congr fun j _ => ?_ + have hcard : card (Fin q) = q := rfl + have hdig : (enc c / q ^ j.val) % q = enc (c j) := by + have h := digit_sum (c := card (Fin q)) hq (encAt c) j.isLt + (fun i hi => encAt_lt c hi) + rw [hcard] at h + rw [show (enc c : ℕ) = ∑ i ∈ Finset.range T, encAt c i * q ^ i from rfl, h, encAt, + dif_pos j.isLt] + rw [hdig] + rfl + +namespace ConstraintGraph + +variable {α : Type} [DecidableEq α] (G : ConstraintGraph α) (E : ExpanderFamily) + +/-- How many darts the preprocessed graph has at each vertex. -/ +noncomputable def preDeg : ℕ := card (G.preprocess E).graph.D + +/-- The vertex reached after `k` steps, following the digits of `s`. -/ +noncomputable def walkNum (G : ConstraintGraph α) (E : ExpanderFamily) (s : ℕ) : + ℕ → ℕ → ℕ + | 0, v => v + | k + 1, v => + (G.preRotNum E (walkNum G E s k v) ((s / G.preDeg E ^ k) % G.preDeg E)).1 + +/-- The digits of a tuple's number are its entries' numbers. -/ +theorem digit_enc {T : ℕ} (s : Fin T → (G.preprocess E).graph.D) (hpos : 0 < G.preDeg E) + (k : ℕ) (hk : k < T) : (enc s / G.preDeg E ^ k) % G.preDeg E = enc (s ⟨k, hk⟩) := by + have h := digit_sum (c := card (G.preprocess E).graph.D) hpos (encAt s) hk + (fun i hi => encAt_lt s hi) + rw [show (enc s : ℕ) + = ∑ i ∈ Finset.range T, encAt s i * card (G.preprocess E).graph.D ^ i from rfl, + preDeg, h, encAt, dif_pos hk] + +/-- **Following the digits walks the graph.** The steps are read from any number +whose digits are the tuple's entries, so a prefix of a longer walk may be run +from that walk's own number. -/ +theorem walkNum_eq {T : ℕ} (v : G.HalfEdge) (s : Fin T → (G.preprocess E).graph.D) (n : ℕ) + (hdig : ∀ (k : ℕ) (hk : k < T), (n / G.preDeg E ^ k) % G.preDeg E = enc (s ⟨k, hk⟩)) : + ∀ {k : ℕ}, k ≤ T → + G.walkNum E n k (enc v) = enc ((G.preprocess E).graph.walkAt T v s k) := by + intro k + induction k with + | zero => intro _; rfl + | succ k ih => + intro hk + have hkT : k < T := by omega + rw [walkNum, ih (by omega), hdig k hkT, + (G.preprocess E).graph.walkAt_succ_of_lt v s hkT, RegGraph.nbr] + exact congrArg Prod.fst (G.preRotNum_eq E _ _) + +/-! ### The dart the walk comes back by -/ + +/-- The dart a killed walk comes back by, in numbers: below the stopping index +the digits are the labels pointing back, read in reverse order; above it they +are the original steps. -/ +noncomputable def killedRevNum (T q v s c : ℕ) : ℕ := + ∑ j ∈ Finset.range T, + (if j < stopAtNum T q c then + (G.preRotNum E (G.walkNum E s (stopAtNum T q c - 1 - j) v) + ((s / G.preDeg E ^ (stopAtNum T q c - 1 - j)) % G.preDeg E)).2 + else (s / G.preDeg E ^ j) % G.preDeg E) * G.preDeg E ^ j + +/-- **The digits give the dart the walk comes back by.** -/ +theorem killedRevNum_eq {T q : ℕ} (hq : 0 < q) (hpos : 0 < G.preDeg E) (v : G.HalfEdge) + (s : Fin T → (G.preprocess E).graph.D) (c : Fin T → Fin q) : + G.killedRevNum E T q (enc v) (enc s) (enc c) + = enc ((G.preprocess E).graph.killedRev v s c) := by + have hdig : ∀ (k : ℕ) (hk : k < T), + (enc s / G.preDeg E ^ k) % G.preDeg E = enc (s ⟨k, hk⟩) := by + intro k hk + have h := digit_sum (c := card (G.preprocess E).graph.D) hpos (encAt s) hk + (fun i hi => encAt_lt s hi) + rw [show (enc s : ℕ) + = ∑ i ∈ Finset.range T, encAt s i * card (G.preprocess E).graph.D ^ i from rfl, + preDeg, h, encAt, dif_pos hk] + have hstop : stopAtNum T q (enc c) = stopAt c := stopAtNum_eq hq c + have hle : stopAt c ≤ T := stopAt_le c + rw [killedRevNum, hstop, + show (enc ((G.preprocess E).graph.killedRev v s c) : ℕ) + = ∑ j ∈ Finset.range T, + encAt ((G.preprocess E).graph.killedRev v s c) j * G.preDeg E ^ j from rfl] + refine Finset.sum_congr rfl fun j hj => ?_ + rw [Finset.mem_range] at hj + congr 1 + rw [encAt, dif_pos hj] + by_cases hjlt : j < stopAt c + · rw [if_pos hjlt] + have hk : stopAt c - 1 - j < T := by omega + rw [hdig _ hk] + have hrev : ((G.preprocess E).graph.killedRev v s c) ⟨j, hj⟩ + = (G.preprocess E).graph.backLabel v + ((G.preprocess E).graph.preWalk s hle) (Fin.rev ⟨j, hjlt⟩) := by + rw [RegGraph.killedRev, RegGraph.extWalk, dif_pos hjlt, RegGraph.revWalk] + rw [hrev, RegGraph.backLabel] + have hidx : (Fin.rev (⟨j, hjlt⟩ : Fin (stopAt c))).val = stopAt c - 1 - j := by + rw [Fin.val_rev] + show stopAt c - (j + 1) = stopAt c - 1 - j + omega + rw [hidx] + have hwalk : ((G.preprocess E).graph.preWalk s hle) (Fin.rev (⟨j, hjlt⟩ : Fin (stopAt c))) + = s ⟨stopAt c - 1 - j, hk⟩ := by + rw [RegGraph.preWalk] + congr 1 + exact Fin.ext hidx + rw [hwalk] + have hpre : ∀ (k : ℕ) (hk : k < stopAt c), + (enc s / G.preDeg E ^ k) % G.preDeg E + = enc (((G.preprocess E).graph.preWalk s hle) ⟨k, hk⟩) := by + intro k hk + rw [G.digit_enc E s hpos k (lt_of_lt_of_le hk hle), RegGraph.preWalk] + rw [G.walkNum_eq E v ((G.preprocess E).graph.preWalk s hle) (enc s) hpre (by omega)] + exact congrArg Prod.snd (G.preRotNum_eq E _ _) + · rw [if_neg hjlt, hdig _ hj] + congr 1 + rw [RegGraph.killedRev, RegGraph.extWalk, dif_neg hjlt] + +/-! ### The powered graph's rotation map -/ + +/-- The powered graph's rotation map, in numbers: walk to the end, come back by +the reversed labels, and keep the coins. -/ +noncomputable def killedRotNum (T q v s c : ℕ) : ℕ × ℕ := + (G.walkNum E s (stopAtNum T q c) v, G.killedRevNum E T q v s c * q ^ T + c) + +/-- **The numbers run the powered graph's rotation map.** -/ +theorem killedRotNum_eq {T q : ℕ} (hq : 0 < q) (hpos : 0 < G.preDeg E) (v : G.HalfEdge) + (x : (Fin T → (G.preprocess E).graph.D) × (Fin T → Fin q)) + {w : G.HalfEdge} {y : (Fin T → (G.preprocess E).graph.D) × (Fin T → Fin q)} + (hw : ((G.preprocess E).graph.killedPower q T hq).rot (v, x) = (w, y)) : + G.killedRotNum E T q (enc v) (enc x.1) (enc x.2) = (enc w, enc y) := by + have hle : stopAt x.2 ≤ T := stopAt_le x.2 + have hstop : stopAtNum T q (enc x.2) = stopAt x.2 := stopAtNum_eq hq x.2 + have hw1 : ((G.preprocess E).graph.killedEnd v x.1 x.2) = w := congrArg Prod.fst hw + have hw2 : (((G.preprocess E).graph.killedRev v x.1 x.2), x.2) = y := congrArg Prod.snd hw + refine Prod.ext ?_ ?_ + · show G.walkNum E (enc x.1) (stopAtNum T q (enc x.2)) (enc v) = _ + rw [hstop] + have hpre : ∀ (k : ℕ) (hk : k < stopAt x.2), + (enc x.1 / G.preDeg E ^ k) % G.preDeg E + = enc (((G.preprocess E).graph.preWalk x.1 hle) ⟨k, hk⟩) := by + intro k hk + rw [G.digit_enc E x.1 hpos k (lt_of_lt_of_le hk hle), RegGraph.preWalk] + rw [G.walkNum_eq E v ((G.preprocess E).graph.preWalk x.1 hle) (enc x.1) hpre le_rfl] + rw [← hw1, RegGraph.killedEnd, ← RegGraph.walkAt_self_eq_walkEnd] + rfl + · show G.killedRevNum E T q (enc v) (enc x.1) (enc x.2) * q ^ T + enc x.2 = _ + rw [G.killedRevNum_eq E hq hpos v x.1 x.2, ← hw2] + rfl + +end ConstraintGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/AlphabetLift.lean b/Complexitylib/Classes/PCP/Internal/AlphabetLift.lean new file mode 100644 index 00000000..116df595 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/AlphabetLift.lean @@ -0,0 +1,99 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.ConstraintGraph + +/-! +# Enlarging the alphabet of a constraint graph + +Dinur's round is an endomorphism of constraint graphs over one fixed alphabet — +the one its composition step produces — while the reduction from 3-SAT lands in +an alphabet of its own. This module bridges the two: an injection of alphabets +carries a constraint graph to a graph over the larger alphabet, keeping the same +vertices and edges, and preserving satisfiability in both directions. + +An edge of the lifted graph accepts a pair of symbols exactly when both are +images and the originals satisfied the original edge. So an assignment using a +symbol outside the image fails every edge at that vertex, and a satisfying +assignment of the lift can be pulled back. + +## Main definitions + +- `Complexity.ConstraintGraph.lift` — the graph over the larger alphabet + +## Main results + +- `Complexity.ConstraintGraph.satisfiable_lift_iff` — satisfiability is preserved +-/ + +@[expose] public section + +namespace Complexity + +namespace ConstraintGraph + +variable {α β : Type} [Fintype α] [DecidableEq β] + +/-- The same graph, read over a larger alphabet along `f`. -/ +def lift (G : ConstraintGraph α) (f : α → β) : ConstraintGraph β where + numVerts := G.numVerts + numEdges := G.numEdges + tail := G.tail + head := G.head + rel := fun e b₁ b₂ => + decide (∃ a₁ : α, ∃ a₂ : α, f a₁ = b₁ ∧ f a₂ = b₂ ∧ G.rel e a₁ a₂ = true) + +@[simp] theorem rel_lift (G : ConstraintGraph α) (f : α → β) (e : Fin (G.lift f).numEdges) + (b₁ b₂ : β) : + (G.lift f).rel e b₁ b₂ + = decide (∃ a₁ : α, ∃ a₂ : α, f a₁ = b₁ ∧ f a₂ = b₂ ∧ G.rel e a₁ a₂ = true) := rfl + +@[simp] theorem tail_lift (G : ConstraintGraph α) (f : α → β) + (e : Fin (G.lift f).numEdges) : (G.lift f).tail e = G.tail e := rfl + +@[simp] theorem head_lift (G : ConstraintGraph α) (f : α → β) + (e : Fin (G.lift f).numEdges) : (G.lift f).head e = G.head e := rfl + +@[simp] theorem numEdges_lift (G : ConstraintGraph α) (f : α → β) : + (G.lift f).numEdges = G.numEdges := rfl + +@[simp] theorem numVerts_lift (G : ConstraintGraph α) (f : α → β) : + (G.lift f).numVerts = G.numVerts := rfl + +theorem satisfies_lift_iff (G : ConstraintGraph α) (f : α → β) + (b : (G.lift f).Assignment) (e : Fin (G.lift f).numEdges) : + (G.lift f).Satisfies b e ↔ + ∃ a₁ : α, ∃ a₂ : α, f a₁ = b (G.tail e) ∧ f a₂ = b (G.head e) + ∧ G.rel e a₁ a₂ = true := by + rw [Satisfies, satisfies] + simp + +/-- **Satisfiability is unchanged.** -/ +theorem satisfiable_lift_iff (G : ConstraintGraph α) {f : α → β} (hf : Function.Injective f) + [Nonempty α] : (G.lift f).Satisfiable ↔ G.Satisfiable := by + classical + constructor + · rintro ⟨b, hb⟩ + refine ⟨fun v => if h : ∃ x : α, f x = b v then h.choose else Classical.arbitrary α, ?_⟩ + intro e + have he := (satisfies_lift_iff G f b e).1 (hb e) + obtain ⟨a₁, a₂, h₁, h₂, hrel⟩ := he + have hex₁ : ∃ x : α, f x = b (G.tail e) := ⟨a₁, h₁⟩ + have hex₂ : ∃ x : α, f x = b (G.head e) := ⟨a₂, h₂⟩ + have hc₁ : hex₁.choose = a₁ := hf (hex₁.choose_spec.trans h₁.symm) + have hc₂ : hex₂.choose = a₂ := hf (hex₂.choose_spec.trans h₂.symm) + show G.rel e _ _ = true + dsimp only + rw [dif_pos hex₁, dif_pos hex₂, hc₁, hc₂] + exact hrel + · rintro ⟨a, ha⟩ + refine ⟨fun v => f (a v), fun e => ?_⟩ + rw [satisfies_lift_iff] + exact ⟨a (G.tail e), a (G.head e), rfl, rfl, ha e⟩ + +end ConstraintGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/Amplification.lean b/Complexitylib/Classes/PCP/Internal/Amplification.lean new file mode 100644 index 00000000..726b5736 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/Amplification.lean @@ -0,0 +1,167 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.ConstraintGraph +public import Mathlib.Tactic.Ring +public import Mathlib.Tactic.Linarith + +/-! +# Gap amplification: the iteration spine of Dinur's proof + +Dinur's proof of the PCP theorem rests on a single transformation of constraint +graphs over a *fixed* alphabet that + +* blows the graph up by at most a constant factor, +* keeps satisfiable graphs satisfiable, and +* **doubles** the unsatisfiability value, until it reaches a universal + constant `gap`. + +This module packages those three properties as `Amplifier` and derives the +consequence that drives everything else: iterating the transformation +logarithmically many times turns *any* unsatisfiable graph into one whose value +is at least `gap`, while a satisfiable graph stays satisfiable. That is the +constant-gap dichotomy an `O(log n)`-randomness, `O(1)`-query verifier needs. + +The construction of an `Amplifier` — degree reduction, expanderization, +powering, and alphabet reduction by composition — is the mathematical content +of the proof and lives in the sibling modules. Everything here is independent +of it, and independent of any machine model: the polynomial-time computability +of the iterated transformation is tracked separately. + +## Main definitions + +- `Amplifier` — the interface above +- `Amplifier.iter` — the `k`-fold iterate + +## Main results + +- `Amplifier.satisfiable_iter` — satisfiability is preserved +- `Amplifier.unsatVal_iter_ge` — the value is at least `min gap (2 ^ k · v)` +- `Amplifier.gap_le_unsatVal_iter` — after `k` rounds with `numEdges ≤ 2 ^ k`, + an unsatisfiable graph has value at least `gap` +- `Amplifier.dichotomy` — the two cases together +-/ + +@[expose] public section + +namespace Complexity + +open ConstraintGraph + +/-- A gap amplifier for constraint graphs over the alphabet `α`: a +size-bounded, satisfiability-preserving transformation that doubles the +unsatisfiability value up to the threshold `gap`. -/ +structure Amplifier (α : Type) [Fintype α] [Nonempty α] where + /-- The transformation on constraint graphs. -/ + transform : ConstraintGraph α → ConstraintGraph α + /-- The constant factor by which the number of edges may grow. -/ + edgeFactor : ℕ + /-- The universal threshold beyond which the value need not grow. -/ + gap : ℚ + /-- The threshold is positive. -/ + gap_pos : 0 < gap + /-- The threshold is at most one, as any unsatisfiability value is. -/ + gap_le_one : gap ≤ 1 + /-- The transformation blows the graph up by at most a constant factor. -/ + numEdges_transform_le : ∀ G, (transform G).numEdges ≤ edgeFactor * G.numEdges + /-- Satisfiable graphs stay satisfiable: this is perfect completeness. -/ + satisfiable_transform : ∀ G, G.Satisfiable → (transform G).Satisfiable + /-- The value doubles, until it reaches `gap`. -/ + unsatVal_transform_ge : ∀ G, min gap (2 * G.unsatVal) ≤ (transform G).unsatVal + +namespace Amplifier + +variable {α : Type} [Fintype α] [Nonempty α] (A : Amplifier α) + +/-- The `k`-fold iterate of the amplifier. -/ +def iter (A : Amplifier α) (k : ℕ) (G : ConstraintGraph α) : ConstraintGraph α := + A.transform^[k] G + +@[simp] theorem iter_zero (G : ConstraintGraph α) : A.iter 0 G = G := rfl + +theorem iter_succ (k : ℕ) (G : ConstraintGraph α) : + A.iter (k + 1) G = A.transform (A.iter k G) := + Function.iterate_succ_apply' _ _ _ + +/-! ### Completeness -/ + +theorem satisfiable_iter {G : ConstraintGraph α} (h : G.Satisfiable) (k : ℕ) : + (A.iter k G).Satisfiable := by + induction k with + | zero => simpa using h + | succ k ih => rw [iter_succ]; exact A.satisfiable_transform _ ih + +/-! ### Soundness -/ + +/-- One doubling step, at the level of the truncated value `min gap ·`. -/ +private theorem min_le_min_two_mul {g X : ℚ} (hg : 0 ≤ g) (hX : 0 ≤ X) : + min g X ≤ min g (2 * min g X) := by + refine le_min (min_le_left _ _) ?_ + have h1 : (0 : ℚ) ≤ min g X := le_min hg hX + linarith + +theorem unsatVal_iter_ge (k : ℕ) (G : ConstraintGraph α) : + min A.gap (2 ^ k * G.unsatVal) ≤ (A.iter k G).unsatVal := by + induction k with + | zero => simp + | succ k ih => + have hstep : min A.gap (2 * (A.iter k G).unsatVal) ≤ (A.iter (k + 1) G).unsatVal := by + rw [iter_succ]; exact A.unsatVal_transform_ge _ + refine le_trans ?_ hstep + have hmono : min A.gap (2 * min A.gap (2 ^ k * G.unsatVal)) + ≤ min A.gap (2 * (A.iter k G).unsatVal) := by + refine le_min (min_le_left _ _) ?_ + have := min_le_right A.gap (2 * min A.gap (2 ^ k * G.unsatVal)) + linarith [this, ih] + refine le_trans ?_ hmono + have hX : (0 : ℚ) ≤ 2 ^ k * G.unsatVal := by + have := G.unsatVal_nonneg + positivity + have := min_le_min_two_mul (g := A.gap) (X := 2 ^ k * G.unsatVal) + (le_of_lt A.gap_pos) hX + calc min A.gap (2 ^ (k + 1) * G.unsatVal) + = min A.gap (2 * (2 ^ k * G.unsatVal)) := by ring_nf + _ ≤ min A.gap (2 * min A.gap (2 ^ k * G.unsatVal)) := by + refine le_min (min_le_left _ _) ?_ + rcases le_total A.gap (2 ^ k * G.unsatVal) with hle | hle + · have : min A.gap (2 ^ k * G.unsatVal) = A.gap := min_eq_left hle + rw [this] + have : min A.gap (2 * (2 ^ k * G.unsatVal)) ≤ A.gap := min_le_left _ _ + linarith [A.gap_pos] + · have : min A.gap (2 ^ k * G.unsatVal) = 2 ^ k * G.unsatVal := min_eq_right hle + rw [this] + exact min_le_right _ _ + +/-- After enough rounds an unsatisfiable graph has value at least `gap`. The +hypothesis `numEdges ≤ 2 ^ k` is what makes `k = O(log (size))` rounds +suffice. -/ +theorem gap_le_unsatVal_iter {G : ConstraintGraph α} (h : ¬ G.Satisfiable) {k : ℕ} + (hk : G.numEdges ≤ 2 ^ k) : A.gap ≤ (A.iter k G).unsatVal := by + refine le_trans ?_ (A.unsatVal_iter_ge k G) + refine le_min (le_refl _) ?_ + have hm : 0 < G.numEdges := numEdges_pos_of_not_satisfiable h + have hmq : (0 : ℚ) < (G.numEdges : ℚ) := by exact_mod_cast hm + have hkq : ((G.numEdges : ℚ)) ≤ 2 ^ k := by exact_mod_cast hk + have hlow : 1 / (G.numEdges : ℚ) ≤ G.unsatVal := inv_numEdges_le_unsatVal h + have h1 : (1 : ℚ) ≤ 2 ^ k * (1 / (G.numEdges : ℚ)) := by + rw [mul_one_div, le_div_iff₀ hmq, one_mul] + exact hkq + have h2 : (2 : ℚ) ^ k * (1 / (G.numEdges : ℚ)) ≤ 2 ^ k * G.unsatVal := by + have : (0 : ℚ) < 2 ^ k := by positivity + exact mul_le_mul_of_nonneg_left hlow (le_of_lt this) + linarith [A.gap_le_one] + +/-- The constant-gap dichotomy delivered by logarithmically many rounds: a +satisfiable graph maps to a satisfiable graph, and an unsatisfiable one to a +graph of value at least `gap`. -/ +theorem dichotomy (G : ConstraintGraph α) {k : ℕ} (hk : G.numEdges ≤ 2 ^ k) : + (G.Satisfiable → (A.iter k G).Satisfiable) ∧ + (¬ G.Satisfiable → A.gap ≤ (A.iter k G).unsatVal) := + ⟨fun h => A.satisfiable_iter h k, fun h => A.gap_le_unsatVal_iter h hk⟩ + +end Amplifier + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/Arithmetize.lean b/Complexitylib/Classes/PCP/Internal/Arithmetize.lean new file mode 100644 index 00000000..64069bcb --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/Arithmetize.lean @@ -0,0 +1,342 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.Hadamard + +/-! +# Arithmetizing a constraint + +The Hadamard tester checks a single quadratic equation over `𝔽₂`. The +constraint it is composed with in Dinur's proof is an arbitrary predicate on a +constant number of bits — the relation of an outer edge, spelled out on the +encodings of the two labels. Two gaps to close, both classical: + +* An arbitrary predicate is not one quadratic equation, but it is a *system* of + them once auxiliary variables are allowed. The system used here is the + **one-hot** encoding: a selector variable for each candidate assignment, + constrained to have exactly one selector set (a linear equation for the sum + and a quadratic one for each pair), only selectors of satisfying candidates + allowed, and the input bits equal to the selected candidate. + +* A system of equations is checked with one query by taking a **random linear + combination**: if any equation fails, the combination fails on exactly half + of the coefficient vectors, because the failing values form a nonzero vector + and a nonzero vector has odd inner product with half of all vectors. + +## Main definitions + +- `Complexity.QuadConstraint.combine` — a linear combination of constraints +- `Complexity.oneHotSystem` — the one-hot system for a set of satisfying + assignments + +## Main results + +- `Complexity.checkValue_combine` — the check of a combination is the + combination of the checks +- `Complexity.forall_checkValue_of_prob` — a combination passing on more than + half the coefficient vectors means every equation passes +- `Complexity.exists_sat_oneHotSystem`, `Complexity.mem_of_sat_oneHotSystem` + — the one-hot system is satisfiable exactly on the given set +-/ + +@[expose] public section + +namespace Complexity + +open BooleanAnalysis + +variable {n : ℕ} + +/-! ### Linearity of the Hadamard table -/ + +/-- Reading a Hadamard table at a sum of vectors. -/ +theorem hadamard_add_arg (a x y : Cube n) : + hadamard a (x + y) = hadamard a x + hadamard a y := by + show ∑ i, a i * (x i + y i) = (∑ i, a i * x i) + (∑ i, a i * y i) + rw [← Finset.sum_add_distrib] + exact Finset.sum_congr rfl fun i _ => by ring + +/-- Reading a Hadamard table at a finite sum of scaled vectors. -/ +theorem hadamard_sum_smul {J : ℕ} (a : Cube n) (c : Cube J) (v : Fin J → Cube n) : + hadamard a (fun i => ∑ j, c j * v j i) = ∑ j, c j * hadamard a (v j) := by + show ∑ i, a i * ∑ j, c j * v j i = ∑ j, c j * ∑ i, a i * v j i + simp only [Finset.mul_sum] + rw [Finset.sum_comm] + refine Finset.sum_congr rfl fun j _ => Finset.sum_congr rfl fun i _ => ?_ + ring + +/-! ### Random linear combinations -/ + +/-- A linear combination of constraints with coefficients `c`. -/ +def QuadConstraint.combine {J : ℕ} (C : Fin J → QuadConstraint n) (c : Cube J) : + QuadConstraint n where + quad := fun p => ∑ j, c j * (C j).quad p + lin := fun i => ∑ j, c j * (C j).lin i + const := ∑ j, c j * (C j).const + +/-- **The check of a combination is the combination of the checks**, when the +tables are Hadamard codewords (so linear). -/ +theorem checkValue_combine {J : ℕ} (a : Cube n) (b : Cube (n * n)) + (C : Fin J → QuadConstraint n) (c : Cube J) : + checkValue (hadamard b) (hadamard a) (QuadConstraint.combine C c) + = hadamard (fun j => checkValue (hadamard b) (hadamard a) (C j)) c := by + show hadamard b (fun p => ∑ j, c j * (C j).quad p) + hadamard a (fun i => ∑ j, c j * (C j).lin i) + + ∑ j, c j * (C j).const + = ∑ j, (hadamard b (C j).quad + hadamard a (C j).lin + (C j).const) * c j + rw [hadamard_sum_smul, hadamard_sum_smul, ← Finset.sum_add_distrib, ← Finset.sum_add_distrib] + refine Finset.sum_congr rfl fun j _ => ?_ + ring + +/-- **Rigidity of the combination.** If the combined check passes on more than +half of the coefficient vectors, every constraint's check passes. -/ +theorem forall_checkValue_of_prob {J : ℕ} (a : Cube n) (b : Cube (n * n)) + (C : Fin J → QuadConstraint n) + (h : 1 / 2 < Pr[fun c : Cube J => + checkValue (hadamard b) (hadamard a) (QuadConstraint.combine C c) = 0]) : + ∀ j, checkValue (hadamard b) (hadamard a) (C j) = 0 := by + classical + by_contra hcon + push Not at hcon + obtain ⟨j, hj⟩ := hcon + have hv : (fun j => checkValue (hadamard b) (hadamard a) (C j)) ≠ 0 := by + intro h0 + exact hj (congrFun h0 j) + have hhalf := prob_hadamard_ne_zero _ hv + have hrw : (fun c : Cube J => + checkValue (hadamard b) (hadamard a) (QuadConstraint.combine C c) = 0) + = fun c : Cube J => + ¬ (hadamard (fun j => checkValue (hadamard b) (hadamard a) (C j)) c ≠ 0) := by + funext c + rw [checkValue_combine] + exact propext not_not.symm + rw [hrw] at h + have hcompl := BooleanAnalysis.Internal.prob_compl + (fun c : Cube J => hadamard (fun j => checkValue (hadamard b) (hadamard a) (C j)) c ≠ 0) + linarith + +/-- The combination of constraints satisfied by `a` is satisfied by `a`. -/ +theorem QuadConstraint.sat_combine {J : ℕ} (C : Fin J → QuadConstraint n) (a : Cube n) + (h : ∀ j, (C j).Sat a) (c : Cube J) : (QuadConstraint.combine C c).Sat a := by + show hadamard (tensorAssign a) (fun p => ∑ j, c j * (C j).quad p) + + hadamard a (fun i => ∑ j, c j * (C j).lin i) + ∑ j, c j * (C j).const = 0 + rw [hadamard_sum_smul, hadamard_sum_smul, ← Finset.sum_add_distrib, ← Finset.sum_add_distrib] + refine Finset.sum_eq_zero fun j _ => ?_ + have hj : hadamard (tensorAssign a) (C j).quad + hadamard a (C j).lin + (C j).const = 0 := + h j + calc c j * hadamard (tensorAssign a) (C j).quad + c j * hadamard a (C j).lin + + c j * (C j).const + = c j * (hadamard (tensorAssign a) (C j).quad + hadamard a (C j).lin + (C j).const) := by + ring + _ = 0 := by rw [hj, mul_zero] + +/-! ### Reading a table at an appended vector -/ + +/-- Reading at an appended vector reads the two blocks separately. -/ +theorem hadamard_append {k t : ℕ} (a : Cube (k + t)) (u : Cube k) (v : Cube t) : + hadamard a (Fin.append u v) = hadamard (leftBlock a) u + hadamard (rightBlock a) v := by + show ∑ i : Fin (k + t), a i * (Fin.append u v) i + = (∑ i : Fin k, a (Fin.castAdd t i) * u i) + ∑ j : Fin t, a (Fin.natAdd k j) * v j + rw [Fin.sum_univ_add] + congr 1 + · exact Finset.sum_congr rfl fun i _ => by rw [Fin.append_left] + · exact Finset.sum_congr rfl fun j _ => by rw [Fin.append_right] + +theorem hadamard_zero_arg (a : Cube n) : hadamard a 0 = 0 := by + show ∑ i, a i * (0 : ZMod 2) = 0 + simp + +theorem hadamard_tensorAssign_basisVec (a : Cube n) (u v : Fin n) : + hadamard (tensorAssign a) (basisVec (finProdFinEquiv (u, v))) = a u * a v := by + rw [hadamard_basisVec] + show a (finProdFinEquiv.symm (finProdFinEquiv (u, v))).1 + * a (finProdFinEquiv.symm (finProdFinEquiv (u, v))).2 = a u * a v + rw [Equiv.symm_apply_apply] + +/-! ### The one-hot system -/ + +/-- Candidate assignments, numbered. -/ +noncomputable def candIdx (k : ℕ) : Cube k ≃ Fin (2 ^ k) := + Fintype.equivFinOfCardEq (card_cube k) + +/-- The selector variable of candidate number `m`. -/ +def auxVar (k : ℕ) (m : Fin (2 ^ k)) : Fin (k + 2 ^ k) := Fin.natAdd k m + +/-- The trivial constraint `0 = 0`. -/ +def QuadConstraint.trivial (n : ℕ) : QuadConstraint n := ⟨0, 0, 0⟩ + +theorem QuadConstraint.sat_trivial (a : Cube n) : (QuadConstraint.trivial n).Sat a := by + show hadamard (tensorAssign a) 0 + hadamard a 0 + 0 = 0 + rw [hadamard_zero_arg, hadamard_zero_arg] + simp + +/-- The index set of the one-hot system: the sum constraint, a constraint per +pair of candidates, a constraint per candidate, and a constraint per input +coordinate. -/ +abbrev OneHotIdx (k : ℕ) : Type := + Unit ⊕ ((Fin (2 ^ k) × Fin (2 ^ k)) ⊕ (Fin (2 ^ k) ⊕ Fin k)) + +/-- The constraints of the one-hot system for the satisfying set `S`. -/ +noncomputable def oneHotOf {k : ℕ} (S : Finset (Cube k)) : OneHotIdx k → QuadConstraint (k + 2 ^ k) + | Sum.inl () => ⟨0, Fin.append 0 (fun _ => 1), 1⟩ + | Sum.inr (Sum.inl (m, m')) => + if m = m' then QuadConstraint.trivial _ + else ⟨basisVec (finProdFinEquiv (auxVar k m, auxVar k m')), 0, 0⟩ + | Sum.inr (Sum.inr (Sum.inl m)) => + if (candIdx k).symm m ∈ S then QuadConstraint.trivial _ + else ⟨0, basisVec (auxVar k m), 0⟩ + | Sum.inr (Sum.inr (Sum.inr i)) => + ⟨0, Fin.append (basisVec i) (fun m => ((candIdx k).symm m) i), 0⟩ + +/-- **The one-hot system**, as a `Fin`-indexed family for `combine`. -/ +noncomputable def oneHotSystem {k : ℕ} (S : Finset (Cube k)) : + Fin (Fintype.card (OneHotIdx k)) → QuadConstraint (k + 2 ^ k) := + fun j => oneHotOf S ((Fintype.equivFin (OneHotIdx k)).symm j) + +theorem forall_oneHotSystem_iff {k : ℕ} (S : Finset (Cube k)) (a : Cube (k + 2 ^ k)) : + (∀ j, (oneHotSystem S j).Sat a) ↔ ∀ x, (oneHotOf S x).Sat a := by + constructor + · intro h x + have := h (Fintype.equivFin (OneHotIdx k) x) + simpa [oneHotSystem] using this + · intro h j + exact h _ + +/-! #### Evaluating the constraints -/ + +theorem sat_oneHot_sum_iff {k : ℕ} (S : Finset (Cube k)) (a : Cube (k + 2 ^ k)) : + (oneHotOf S (Sum.inl ())).Sat a ↔ ∑ m, rightBlock a m = 1 := by + show hadamard (tensorAssign a) 0 + hadamard a (Fin.append 0 (fun _ => 1)) + 1 = 0 ↔ _ + rw [hadamard_zero_arg, hadamard_append, hadamard_zero_arg] + have h : hadamard (rightBlock a) (fun _ => 1) = ∑ m, rightBlock a m := by + show ∑ m, rightBlock a m * 1 = _ + simp + rw [h] + rcases (by decide : ∀ u : ZMod 2, (0 + (0 + u) + 1 = 0) ↔ (u = 1)) (∑ m, rightBlock a m) + with h' + exact h' + +theorem sat_oneHot_pair_iff {k : ℕ} (S : Finset (Cube k)) (a : Cube (k + 2 ^ k)) + (m m' : Fin (2 ^ k)) (hne : m ≠ m') : + (oneHotOf S (Sum.inr (Sum.inl (m, m')))).Sat a ↔ rightBlock a m * rightBlock a m' = 0 := by + unfold QuadConstraint.Sat QuadConstraint.eval + simp only [oneHotOf, if_neg hne] + rw [hadamard_tensorAssign_basisVec, hadamard_zero_arg, add_zero, add_zero] + rfl + +theorem sat_oneHot_allowed_iff {k : ℕ} (S : Finset (Cube k)) (a : Cube (k + 2 ^ k)) + (m : Fin (2 ^ k)) (hm : (candIdx k).symm m ∉ S) : + (oneHotOf S (Sum.inr (Sum.inr (Sum.inl m)))).Sat a ↔ rightBlock a m = 0 := by + unfold QuadConstraint.Sat QuadConstraint.eval + simp only [oneHotOf, if_neg hm] + rw [hadamard_zero_arg, hadamard_basisVec, zero_add, add_zero] + rfl + +theorem sat_oneHot_coord_iff {k : ℕ} (S : Finset (Cube k)) (a : Cube (k + 2 ^ k)) (i : Fin k) : + (oneHotOf S (Sum.inr (Sum.inr (Sum.inr i)))).Sat a + ↔ leftBlock a i + ∑ m, rightBlock a m * ((candIdx k).symm m) i = 0 := by + show hadamard (tensorAssign a) 0 + + hadamard a (Fin.append (basisVec i) (fun m => ((candIdx k).symm m) i)) + 0 = 0 ↔ _ + rw [hadamard_zero_arg, hadamard_append, hadamard_basisVec, zero_add, add_zero] + rfl + +/-! #### Exactly one selector -/ + +/-- A `0`/`1` vector with odd sum and pairwise zero products is a basis vector. -/ +theorem eq_basis_of_sum_one {T : ℕ} (y : Fin T → ZMod 2) (hsum : ∑ m, y m = 1) + (hpair : ∀ m m', m ≠ m' → y m * y m' = 0) : + ∃ m₀, y m₀ = 1 ∧ ∀ m, m ≠ m₀ → y m = 0 := by + classical + have hex : ∃ m₀, y m₀ ≠ 0 := by + by_contra hcon + push Not at hcon + have : ∑ m, y m = 0 := Finset.sum_eq_zero fun m _ => hcon m + rw [this] at hsum + exact absurd hsum (by decide) + obtain ⟨m₀, hm₀⟩ := hex + have hone : y m₀ = 1 := by + rcases (by decide : ∀ u : ZMod 2, u ≠ 0 → u = 1) (y m₀) hm₀ with h + exact h + refine ⟨m₀, hone, fun m hm => ?_⟩ + have := hpair m m₀ hm + rw [hone, mul_one] at this + exact this + +/-! #### The characterization -/ + +/-- **Satisfying the one-hot system puts the input in the set.** -/ +theorem mem_of_sat_oneHotSystem {k : ℕ} (S : Finset (Cube k)) (a : Cube (k + 2 ^ k)) + (h : ∀ j, (oneHotSystem S j).Sat a) : leftBlock a ∈ S := by + classical + rw [forall_oneHotSystem_iff] at h + have hsum := (sat_oneHot_sum_iff S a).1 (h (Sum.inl ())) + obtain ⟨m₀, hm₀, hother⟩ := eq_basis_of_sum_one (rightBlock a) hsum + fun m m' hne => (sat_oneHot_pair_iff S a m m' hne).1 (h _) + have hmem : (candIdx k).symm m₀ ∈ S := by + by_contra hnot + have := (sat_oneHot_allowed_iff S a m₀ hnot).1 (h _) + rw [hm₀] at this + exact absurd this (by decide) + have hw : leftBlock a = (candIdx k).symm m₀ := by + funext i + have hc := (sat_oneHot_coord_iff S a i).1 (h _) + rw [Finset.sum_eq_single m₀ (fun m _ hm => by rw [hother m hm, zero_mul]) + (fun hn => absurd (Finset.mem_univ m₀) hn), hm₀, one_mul] at hc + rcases (by decide : ∀ u v : ZMod 2, u + v = 0 → u = v) _ _ hc with h' + exact h' + rw [hw] + exact hmem + +/-- The honest extension of an input: select its own candidate. -/ +noncomputable def oneHotExtend {k : ℕ} (w : Cube k) : Cube (k + 2 ^ k) := + Fin.append w (fun m => if m = candIdx k w then 1 else 0) + +theorem leftBlock_oneHotExtend {k : ℕ} (w : Cube k) : leftBlock (oneHotExtend w) = w := by + funext i + unfold leftBlock oneHotExtend + rw [Fin.append_left] + +theorem rightBlock_oneHotExtend {k : ℕ} (w : Cube k) (m : Fin (2 ^ k)) : + rightBlock (oneHotExtend w) m = if m = candIdx k w then 1 else 0 := by + unfold rightBlock oneHotExtend + rw [Fin.append_right] + +/-- **An input in the set satisfies the one-hot system**, once extended by its +own selector. -/ +theorem sat_oneHotSystem_extend {k : ℕ} (S : Finset (Cube k)) (w : Cube k) (hw : w ∈ S) : + ∀ j, (oneHotSystem S j).Sat (oneHotExtend w) := by + classical + rw [forall_oneHotSystem_iff] + intro x + rcases x with _ | ⟨⟨m, m'⟩ | m | i⟩ + · rw [sat_oneHot_sum_iff] + rw [Finset.sum_eq_single (candIdx k w) + (fun m _ hm => by rw [rightBlock_oneHotExtend, if_neg hm]) + (fun hn => absurd (Finset.mem_univ _) hn), rightBlock_oneHotExtend, if_pos rfl] + · by_cases hne : m = m' + · simp only [oneHotOf, if_pos hne] + exact QuadConstraint.sat_trivial _ + · rw [sat_oneHot_pair_iff S _ m m' hne, rightBlock_oneHotExtend, rightBlock_oneHotExtend] + by_cases hm : m = candIdx k w + · have hm' : m' ≠ candIdx k w := fun h => hne (hm.trans h.symm) + rw [if_neg hm', mul_zero] + · rw [if_neg hm, zero_mul] + · by_cases hm : (candIdx k).symm m ∈ S + · simp only [oneHotOf, if_pos hm] + exact QuadConstraint.sat_trivial _ + · rw [sat_oneHot_allowed_iff S _ m hm, rightBlock_oneHotExtend, if_neg] + intro hmw + apply hm + rw [hmw, Equiv.symm_apply_apply] + exact hw + · rw [sat_oneHot_coord_iff, leftBlock_oneHotExtend] + rw [Finset.sum_eq_single (candIdx k w) + (fun m _ hm => by rw [rightBlock_oneHotExtend, if_neg hm, zero_mul]) + (fun hn => absurd (Finset.mem_univ _) hn), rightBlock_oneHotExtend, if_pos rfl, + one_mul, Equiv.symm_apply_apply] + rcases (by decide : ∀ u : ZMod 2, u + u = 0) (w i) with h' + exact h' + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/BaseAlg.lean b/Complexitylib/Classes/PCP/Internal/BaseAlg.lean new file mode 100644 index 00000000..e75ec33e --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/BaseAlg.lean @@ -0,0 +1,390 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.PCP.Internal.CNFMaxVar +public import Complexitylib.Classes.PCP.Internal.GapReduction +public import Complexitylib.Classes.PCP.Internal.FiniteKey +public import Complexitylib.Classes.PCP.Internal.SymbolCodec +public import Complexitylib.Classes.PCP.Internal.CSPVerifier +public import Complexitylib.Classes.PCP.Internal.AlgCSPModel + +/-! +# The base graph, as an algorithm sees it + +The constraint graph of a 3CNF formula has three edges per clause, a vertex per +variable and a vertex per clause. This module computes those numbers and those +endpoints from the formula's encoding, in polynomial time. + +The vertex numbering puts variables first, so a clause vertex sits at +`maxVar + 1 + j`; both quantities are read off the encoding by the loops of +`MaxLoop` and the parser. + +## Main definitions + +- `Complexity.baseEdgesU` — the edge count, in unary +- `Complexity.baseMaxU` — the largest variable index, in unary + +## Main results + +- `Complexity.baseEdgesU_eq`, `Complexity.baseMaxU_eq` — what they compute +- `Complexity.baseTailU_eq`, `Complexity.baseHeadU_eq` — the two endpoints +- `Complexity.baseKey_mem_FP`, `Complexity.length_baseKey_le` — the constraint's + bounded key +- `Complexity.baseOk_mem_P` — the constraint is polynomial-time decidable +- `Complexity.baseAlg` — the base graph as an `AlgCSP` +- `Complexity.card_gapAlpha` — the alphabet fits in `23` bits +- `Complexity.litSignFn_encode` — the sign flag is the literal's sign +-/ + +@[expose] public section + +namespace Complexity + +open SAT ThreeSATCSP + +variable (E : List Bool → List Bool) + +/-- The number of edges, in unary: three per clause. -/ +noncomputable def baseEdgesU (z : List Bool) : List Bool := + clauseCountFn (E z) ++ clauseCountFn (E z) ++ clauseCountFn (E z) + +theorem baseEdgesU_mem_FP (hE : E ∈ FP) : baseEdgesU E ∈ FP := by + have hc : (fun z => clauseCountFn (E z)) ∈ FP := by + have := mem_FP_comp hE clauseCountFn_mem_FP + refine mem_FP_of_eq this fun z => ?_ + rw [Function.comp_apply] + exact Cobham.appendFn_mem_FP (Cobham.appendFn_mem_FP hc hc) hc + +theorem baseEdgesU_eq {Φ : List Bool → CNF} (hE : ∀ x, E x = (Φ x).encode) (x : List Bool) : + baseEdgesU E x = List.replicate (3 * (Φ x).length) true := by + rw [baseEdgesU, hE, clauseCountFn_eq (even_length_encode _), sepCount_encode] + rw [← List.replicate_add, ← List.replicate_add] + congr 1 + ring + +/-- The largest variable index, in unary. -/ +noncomputable def baseMaxU (z : List Bool) : List Bool := + maxFn slotVar (pair (baseEdgesU E z) (E z)) + +theorem baseMaxU_mem_FP (hE : E ∈ FP) : baseMaxU E ∈ FP := by + have hpair : (fun z => pair (baseEdgesU E z) (E z)) ∈ FP := + Cobham.pairFn_mem_FP (baseEdgesU_mem_FP E hE) hE + have := mem_FP_comp hpair (maxFn_mem_FP slotVar_mem_FP) + refine mem_FP_of_eq this fun z => ?_ + rw [Function.comp_apply, baseMaxU] + +theorem baseMaxU_eq {Φ : List Bool → CNF} (hE : ∀ x, E x = (Φ x).encode) + (h3 : ∀ x, CNF.Is3CNF (Φ x)) (x : List Bool) : + (baseMaxU E x).length = (Φ x).maxVar := by + rw [baseMaxU, baseEdgesU_eq E hE, hE, maxFn_eq, maxOver_slotVar _ (h3 x)] + +/-- The unary form of the first endpoint: the clause vertex. -/ +noncomputable def baseTailU (w : List Bool) : List Bool := + baseMaxU E (pairFst w) ++ [true] + ++ List.replicate (divFn [false, false, false] (pairSnd w)).length true + +/-- The unary form of the second endpoint: the variable vertex. -/ +noncomputable def baseHeadU (w : List Bool) : List Bool := + slotVar (pair (E (pairFst w)) (pairSnd w)) + +theorem baseTailU_mem_FP (hE : E ∈ FP) : baseTailU E ∈ FP := by + have hm : (fun w => baseMaxU E (pairFst w)) ∈ FP := by + have := mem_FP_comp Cobham.fstBlock_mem_FP (baseMaxU_mem_FP E hE) + refine mem_FP_of_eq this fun w => ?_ + rw [Function.comp_apply] + have hd : (fun w : List Bool => + List.replicate (divFn [false, false, false] (pairSnd w)).length true) ∈ FP := by + have h1 : (fun w : List Bool => divFn [false, false, false] (pairSnd w)) ∈ FP := by + have := mem_FP_comp Cobham.sndBlock_mem_FP (divFn_mem_FP [false, false, false]) + refine mem_FP_of_eq this fun w => ?_ + rw [Function.comp_apply] + have := mem_FP_comp h1 unaryLength_mem_FP + refine mem_FP_of_eq this fun w => ?_ + rw [Function.comp_apply] + exact Cobham.appendFn_mem_FP (Cobham.appendFn_mem_FP hm (constFn_mem_FP [true])) hd + +theorem baseHeadU_mem_FP (hE : E ∈ FP) : baseHeadU E ∈ FP := by + have hp : (fun w => pair (E (pairFst w)) (pairSnd w)) ∈ FP := by + refine Cobham.pairFn_mem_FP ?_ Cobham.sndBlock_mem_FP + have := mem_FP_comp Cobham.fstBlock_mem_FP hE + refine mem_FP_of_eq this fun w => ?_ + rw [Function.comp_apply] + have := mem_FP_comp hp slotVar_mem_FP + refine mem_FP_of_eq this fun w => ?_ + rw [Function.comp_apply, baseHeadU] + +/-! ### What the endpoints compute -/ + +theorem baseTailU_eq {Φ : List Bool → CNF} (hE : ∀ x, E x = (Φ x).encode) + (h3 : ∀ x, CNF.Is3CNF (Φ x)) (x : List Bool) (e : ℕ) : + (baseTailU E (pair x (List.replicate e true))).length + = ((Φ x).maxVar + 1) + e / 3 := by + rw [baseTailU, pairFst_pair, pairSnd_pair, List.length_append, + List.length_append, List.length_replicate, List.length_singleton, + divFn_eq (by simp) (List.replicate e true), List.length_replicate, + List.length_replicate, baseMaxU_eq E hE h3] + congr 1 + +theorem baseHeadU_eq {Φ : List Bool → CNF} (hE : ∀ x, E x = (Φ x).encode) + (h3 : ∀ x, CNF.Is3CNF (Φ x)) (x : List Bool) {e : ℕ} (he : e < 3 * (Φ x).length) : + (baseHeadU E (pair x (List.replicate e true))).length + = (litOf (Φ x) (e / 3) ⟨e % 3, Nat.mod_lt _ (by omega)⟩).var := by + have hj : e / 3 < (Φ x).length := by omega + have hp : e % 3 < ((Φ x)[e / 3]'hj).length := by + rw [h3 x _ (List.getElem_mem hj)] + omega + rw [baseHeadU, pairFst_pair, pairSnd_pair, hE, + slotVar_eq (Φ x) hj hp rfl rfl] + congr 1 + rw [litOf, List.getElem?_eq_getElem hj] + simp only [Option.getD_some] + rw [List.getD_eq_getElem?_getD, List.getElem?_eq_getElem hp] + rfl + +/-! ### The key the constraint looks at -/ + +/-- The three literal signs of the clause an edge belongs to. -/ +noncomputable def baseSigns (z : List Bool) : List Bool := + let j := divFn [false, false, false] (pairSnd (pairFst z)) + let x := pairFst (pairFst z) + litSignFn (pair (pair j []) (E x)) + ++ litSignFn (pair (pair j [true]) (E x)) + ++ litSignFn (pair (pair j [true, true]) (E x)) + +/-- Everything the constraint depends on: the clause's signs, the position +inside the clause, and the two symbol blocks. -/ +noncomputable def baseKey (w : ℕ) (z : List Bool) : List Bool := + pair (pair (baseSigns E z) + (modFn [false, false, false] (pairSnd (pairFst z)))) + ((pairSnd z).take (2 * w)) + +theorem baseSigns_mem_FP (hE : E ∈ FP) : baseSigns E ∈ FP := by + have hx : (fun z : List Bool => E (pairFst (pairFst z))) ∈ FP := by + have := mem_FP_comp (mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP) hE + refine mem_FP_of_eq this fun z => ?_ + rw [Function.comp_apply, Function.comp_apply] + have hj : (fun z : List Bool => + divFn [false, false, false] (pairSnd (pairFst z))) ∈ FP := by + have := mem_FP_comp (mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP) + (divFn_mem_FP [false, false, false]) + refine mem_FP_of_eq this fun z => ?_ + rw [Function.comp_apply, Function.comp_apply] + have hsign : ∀ c : List Bool, (fun z : List Bool => + litSignFn (pair (pair (divFn [false, false, false] + (pairSnd (pairFst z))) c) + (E (pairFst (pairFst z))))) ∈ FP := by + intro c + have := mem_FP_comp + (Cobham.pairFn_mem_FP (Cobham.pairFn_mem_FP hj (constFn_mem_FP c)) hx) litSignFn_mem_FP + refine mem_FP_of_eq this fun z => ?_ + rw [Function.comp_apply] + exact Cobham.appendFn_mem_FP (Cobham.appendFn_mem_FP (hsign []) (hsign [true])) + (hsign [true, true]) + +theorem baseKey_mem_FP (hE : E ∈ FP) (w : ℕ) : baseKey E w ∈ FP := by + have hm : (fun z : List Bool => + modFn [false, false, false] (pairSnd (pairFst z))) ∈ FP := by + have := mem_FP_comp (mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP) + (modFn_mem_FP [false, false, false]) + refine mem_FP_of_eq this fun z => ?_ + rw [Function.comp_apply, Function.comp_apply] + have ht : (fun z : List Bool => (pairSnd z).take (2 * w)) ∈ FP := by + have := Cobham.takeLenFn_mem_FP + (constFn_mem_FP (List.replicate (2 * w) false)) Cobham.sndBlock_mem_FP + refine mem_FP_of_eq this fun z => ?_ + rw [List.length_replicate] + exact Cobham.pairFn_mem_FP (Cobham.pairFn_mem_FP (baseSigns_mem_FP E hE) hm) ht + +theorem length_baseSigns_le (z : List Bool) : (baseSigns E z).length ≤ 3 := by + have hs : ∀ y, (litSignFn y).length ≤ 1 := by + intro y + rw [litSignFn, Cobham.selectHead] + split + · simp + · split <;> simp + rw [baseSigns, List.length_append, List.length_append] + have h1 := hs (pair (pair (divFn [false, false, false] + (pairSnd (pairFst z))) []) (E (pairFst (pairFst z)))) + have h2 := hs (pair (pair (divFn [false, false, false] + (pairSnd (pairFst z))) [true]) (E (pairFst (pairFst z)))) + have h3 := hs (pair (pair (divFn [false, false, false] + (pairSnd (pairFst z))) [true, true]) + (E (pairFst (pairFst z)))) + omega + +theorem length_baseKey_le (w : ℕ) (z : List Bool) : + (baseKey E w z).length ≤ 2 * w + 22 := by + have hs := length_baseSigns_le E z + have hm : (modFn [false, false, false] + (pairSnd (pairFst z))).length ≤ 2 := by + rw [modFn_eq (by simp)] + simp only [List.length_replicate, + show ([false, false, false] : List Bool).length = 3 from rfl] + omega + have ht : ((pairSnd z).take (2 * w)).length ≤ 2 * w := by + rw [List.length_take] + omega + rw [baseKey, pair_length, pair_length] + omega + +/-! ### The constraint -/ + +open Classical in +/-- What the constraint says, as a predicate of the key alone: the two blocks +name symbols in the image of the alphabet embedding whose preimages satisfy the +clause and agree on the checked position. -/ +noncomputable def baseOkKey (w : ℕ) (k : List Bool) : Prop := + ∃ a₁ a₂ : Fin 3 → Bool, + alphaEmb a₁ = symDec GapAlpha ((pairSnd k).take w) ∧ + alphaEmb a₂ = symDec GapAlpha ((pairSnd k).drop w) ∧ + (∃ q : Fin 3, a₁ q = (pairFst (pairFst k)).getD q.val false) ∧ + a₁ ⟨(pairSnd (pairFst k)).length % 3, + Nat.mod_lt _ (by omega)⟩ = a₂ 0 + +/-- The constraint, as a language on the verifier's verdict argument. -/ +noncomputable def baseOk (w : ℕ) : Language := + {z : List Bool | baseOkKey w (baseKey E w z)} + +theorem baseOk_mem_P (hE : E ∈ FP) (w : ℕ) : baseOk E w ∈ P := + mem_P_of_bounded_key (baseKey_mem_FP E hE w) (length_baseKey_le E w) (baseOkKey w) + +/-! ### The record -/ + +/-- **The base graph as an algorithm.** -/ +noncomputable def baseAlg (hE : E ∈ FP) : AlgCSP where + numEdges x := (baseEdgesU E x).length + numEdges_mem := by + have := mem_FP_comp (baseEdgesU_mem_FP E hE) unaryLength_mem_FP + refine mem_FP_of_eq this fun x => ?_ + rw [Function.comp_apply] + width := 23 + width_pos := by omega + vert b x e := + cond b (baseHeadU E (pair x (List.replicate e true))).length + (baseTailU E (pair x (List.replicate e true))).length + vert_mem := by + intro b + have hu : (fun w : List Bool => List.replicate (pairSnd w).length true) ∈ FP := by + have := mem_FP_comp Cobham.sndBlock_mem_FP unaryLength_mem_FP + refine mem_FP_of_eq this fun w => ?_ + rw [Function.comp_apply] + have harg : (fun w : List Bool => + pair (pairFst w) (List.replicate (pairSnd w).length true)) ∈ FP := + Cobham.pairFn_mem_FP Cobham.fstBlock_mem_FP hu + cases b + · have := mem_FP_comp (mem_FP_comp harg (baseTailU_mem_FP E hE)) unaryLength_mem_FP + refine mem_FP_of_eq this fun w => ?_ + rw [Function.comp_apply, Function.comp_apply] + simp only [cond_false] + · have := mem_FP_comp (mem_FP_comp harg (baseHeadU_mem_FP E hE)) unaryLength_mem_FP + refine mem_FP_of_eq this fun w => ?_ + rw [Function.comp_apply, Function.comp_apply] + simp only [cond_true] + ok := baseOk E 23 + ok_mem := baseOk_mem_P E hE 23 + +@[simp] theorem numEdges_baseAlg (hE : E ∈ FP) (x : List Bool) : + (baseAlg E hE).numEdges x = (baseEdgesU E x).length := rfl + +@[simp] theorem width_baseAlg (hE : E ∈ FP) : (baseAlg E hE).width = 23 := rfl + +theorem vert_baseAlg_false (hE : E ∈ FP) (x : List Bool) (e : ℕ) : + (baseAlg E hE).vert false x e + = (baseTailU E (pair x (List.replicate e true))).length := by + simp only [baseAlg, cond_false] + +theorem vert_baseAlg_true (hE : E ∈ FP) (x : List Bool) (e : ℕ) : + (baseAlg E hE).vert true x e + = (baseHeadU E (pair x (List.replicate e true))).length := by + simp only [baseAlg, cond_true] + +@[simp] theorem ok_baseAlg (hE : E ∈ FP) : (baseAlg E hE).ok = baseOk E 23 := rfl + +/-! ### The symbol codec fits -/ + +theorem card_gapAlpha : Fintype.card GapAlpha = 2 ^ 23 := by + show Fintype.card (ZMod 2 × (ReadIdx → ZMod 2)) = 2 ^ 23 + rw [Fintype.card_prod, Fintype.card_fun, ZMod.card, card_readIdx] + norm_num + +theorem length_symEnc_gapAlpha (s : GapAlpha) : (symEnc GapAlpha 23 s).length = 23 := + length_symEnc 23 s + +theorem symDec_symEnc_gapAlpha (s : GapAlpha) : + symDec GapAlpha (symEnc GapAlpha 23 s) = s := + symDec_symEnc (by rw [card_gapAlpha]) s + +/-! ### The sign flag -/ + +theorem litSignFn_encode (φ : CNF) {j p : ℕ} (hj : j < φ.length) + (hp : p < (φ[j]'hj).length) : + litSignFn (pair (pair (List.replicate j true) (List.replicate p true)) φ.encode) + = [((φ[j]'hj)[p]'hp).sign] := by + have hseg : litSegFn (pair (pair (List.replicate j true) (List.replicate p true)) φ.encode) + = encodeTokens (Lit.rawTokens ((φ[j]'hj)[p]'hp)) := litSegFn_encode φ hj hp + have hhead : encodeTokens (Lit.rawTokens ((φ[j]'hj)[p]'hp)) + = ((φ[j]'hj)[p]'hp).sign :: ((φ[j]'hj)[p]'hp).sign + :: encodeTokens ((Lit.encodeRaw ((φ[j]'hj)[p]'hp)).tail.map EncToken.bit) := by + rw [Lit.rawTokens, Lit.encodeRaw] + simp only [List.map_cons, List.tail_cons] + rw [encodeTokens_cons] + cases h : ((φ[j]'hj)[p]'hp).sign <;> rfl + rw [litSignFn, hseg, hhead, selectHead_cons] + cases ((φ[j]'hj)[p]'hp).sign <;> simp + +theorem litOf_eq (φ : CNF) {j : ℕ} (hj : j < φ.length) (p : Fin 3) + (hq : p.val < (φ[j]'hj).length) : litOf φ j p = (φ[j]'hj)[p.val]'hq := by + rw [litOf, List.getElem?_eq_getElem hj] + simp only [Option.getD_some] + rw [List.getD_eq_getElem?_getD, List.getElem?_eq_getElem hq] + rfl + +/-! ### Agreement with the real graph -/ + +variable {Φ : List Bool → CNF} + +/-! ### The key on a well-formed argument -/ + +theorem baseSigns_pair (hE : ∀ x, E x = (Φ x).encode) (h3 : ∀ x, CNF.Is3CNF (Φ x)) + (x : List Bool) {e : ℕ} (he : e < 3 * (Φ x).length) (a : List Bool) : + baseSigns E (pair (pair x (List.replicate e true)) a) + = [(litOf (Φ x) (e / 3) 0).sign, (litOf (Φ x) (e / 3) 1).sign, + (litOf (Φ x) (e / 3) 2).sign] := by + have hj : e / 3 < (Φ x).length := by omega + have hlen : ((Φ x)[e / 3]'hj).length = 3 := h3 x _ (List.getElem_mem hj) + have hsign : ∀ q : Fin 3, + litSignFn (pair (pair (List.replicate (e / 3) true) + (List.replicate q.val true)) (E x)) = [(litOf (Φ x) (e / 3) q).sign] := by + intro q + have hq : q.val < ((Φ x)[e / 3]'hj).length := by + rw [hlen] + exact q.isLt + rw [hE, litSignFn_encode (Φ x) hj hq, litOf_eq (Φ x) hj q hq] + rw [baseSigns, pairFst_pair, pairSnd_pair, pairFst_pair, + divFn_eq (by simp) (List.replicate e true), List.length_replicate, + show ([false, false, false] : List Bool).length = 3 from rfl] + have h0 : litSignFn (pair (pair (List.replicate (e / 3) true) []) (E x)) + = [(litOf (Φ x) (e / 3) 0).sign] := hsign 0 + have h1 : litSignFn (pair (pair (List.replicate (e / 3) true) [true]) (E x)) + = [(litOf (Φ x) (e / 3) 1).sign] := hsign 1 + have h2 : litSignFn (pair (pair (List.replicate (e / 3) true) [true, true]) (E x)) + = [(litOf (Φ x) (e / 3) 2).sign] := hsign 2 + rw [h0, h1, h2] + rfl + +theorem baseKey_pair (hE : ∀ x, E x = (Φ x).encode) (h3 : ∀ x, CNF.Is3CNF (Φ x)) + (x : List Bool) {e : ℕ} (he : e < 3 * (Φ x).length) {a : List Bool} + (ha : a.length = 46) : + baseKey E 23 (pair (pair x (List.replicate e true)) a) + = pair (pair [(litOf (Φ x) (e / 3) 0).sign, (litOf (Φ x) (e / 3) 1).sign, + (litOf (Φ x) (e / 3) 2).sign] (List.replicate (e % 3) true)) a := by + rw [baseKey, baseSigns_pair E hE h3 x he a, pairFst_pair, pairSnd_pair, + pairSnd_pair, modFn_eq (by simp) (List.replicate e true), + List.length_replicate, show ([false, false, false] : List Bool).length = 3 from rfl, + List.take_of_length_le (by omega)] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/BinToUnary.lean b/Complexitylib/Classes/PCP/Internal/BinToUnary.lean new file mode 100644 index 00000000..f1c02713 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/BinToUnary.lean @@ -0,0 +1,120 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.BinValLE +public import Complexitylib.Classes.PCP.Internal.CoinEnum + +/-! +# Counting out the value of a bit string + +A verifier turns its coins into an index — into a list of edges, say — and an +index has to be counted out in unary before a polynomial-time loop can use it. +This module does that: it reads a bit string as a little-endian binary number +and writes that many marks. + +The conversion is only polynomial time when the value is, which is why the +result is clamped: the fold's state is truncated to a width the caller supplies. +On strings short enough for the clamp — logarithmically many coins, say — the +answer is exact. + +## Main definitions + +- `Complexity.unaryVal` — the value of a bit string, in unary + +## Main results + +- `Complexity.unaryVal_eq` — it is exact when the clamp is wide enough +- `Complexity.unaryVal_mem_FP` — it is polynomial time +-/ + +@[expose] public section + +namespace Complexity + +/-- Reading a zero: the value doubles. -/ +def binDbl (z : List Bool) : List Bool := + pairSnd (pairFst z) ++ pairSnd (pairFst z) + +/-- Reading a one: the value doubles and gains one. -/ +def binDblOne (z : List Bool) : List Bool := + pairSnd (pairFst z) ++ pairSnd (pairFst z) ++ [true] + +theorem binDbl_mem_FP : binDbl ∈ FP := by + have h : (fun z : List Bool => pairSnd (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + exact Cobham.appendFn_mem_FP h h + +theorem binDblOne_mem_FP : binDblOne ∈ FP := by + have h : (fun z : List Bool => pairSnd (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + exact Cobham.appendFn_mem_FP (Cobham.appendFn_mem_FP h h) (constFn_mem_FP [true]) + +/-- **The value of a bit string, in unary**, computed on `pair anything bits`. -/ +def unaryVal (p : Polynomial ℕ) (z : List Bool) : List Bool := + Cobham.recFoldClamp binDbl binDblOne (p.eval z.length) [] (pairFst z) + (pairSnd z) + +theorem unaryVal_mem_FP (p : Polynomial ℕ) : unaryVal p ∈ FP := + Cobham.recFoldClamp_mem_FP binDbl_mem_FP binDblOne_mem_FP (constFn_mem_FP []) p + +theorem binValLE_cons_false (l : List Bool) : binValLE (false :: l) = 2 * binValLE l := by + rw [binValLE]; simp + +theorem binValLE_cons_true (l : List Bool) : + binValLE (true :: l) = 2 * binValLE l + 1 := by + rw [binValLE]; simp; omega + +/-- The fold really counts out the value, as long as the clamp is wide enough. -/ +theorem recFoldClamp_binValLE (bound : ℕ) (W : List Bool) : + ∀ l : List Bool, 2 ^ l.length ≤ bound → + Cobham.recFoldClamp binDbl binDblOne bound [] W l + = List.replicate (binValLE l) true := by + intro l + induction l with + | nil => + intro _ + rw [Cobham.recFoldClamp] + simp [binValLE] + | cons b l ih => + intro hb + have hb' : 2 ^ l.length ≤ bound := by + have : 2 ^ l.length ≤ 2 ^ (b :: l).length := + Nat.pow_le_pow_right (by omega) (by simp) + omega + have hval : binValLE l < 2 ^ l.length := binValLE_lt l + have hlen : 2 ^ (l.length + 1) ≤ bound := by + have : (b :: l).length = l.length + 1 := by simp + omega + rw [Cobham.recFoldClamp, ih hb'] + have hstate : pairSnd (pairFst + (pair (pair W (List.replicate (binValLE l) true)) l)) + = List.replicate (binValLE l) true := by + rw [pairFst_pair, pairSnd_pair] + cases b with + | false => + show (binDbl _).take bound = _ + rw [binDbl, hstate, binValLE_cons_false, two_mul, ← List.replicate_add] + refine List.take_of_length_le ?_ + rw [List.length_replicate] + omega + | true => + show (binDblOne _).take bound = _ + rw [binDblOne, hstate, binValLE_cons_true, two_mul, + show List.replicate (binValLE l) true ++ List.replicate (binValLE l) true ++ [true] + = List.replicate (binValLE l + binValLE l + 1) true from by + rw [List.replicate_add, List.replicate_add] + rfl] + refine List.take_of_length_le ?_ + rw [List.length_replicate] + omega + +/-- **The conversion is exact** when the clamp is wide enough for the value. -/ +theorem unaryVal_eq {p : Polynomial ℕ} {z : List Bool} + (h : 2 ^ (pairSnd z).length ≤ p.eval z.length) : + unaryVal p z = List.replicate (binValLE (pairSnd z)) true := + recFoldClamp_binValLE _ _ _ h + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/BitwiseFP.lean b/Complexitylib/Classes/PCP/Internal/BitwiseFP.lean new file mode 100644 index 00000000..4280da91 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/BitwiseFP.lean @@ -0,0 +1,105 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal +public import Complexitylib.Classes.P.UnaryLength +public import Complexitylib.Classes.P.PairWithInput +public import Complexitylib.Classes.P.Composition +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P.Cobham.Internal.PVerdict + +/-! +# Computing an output one bit at a time + +A polynomial-time function is usually easiest to describe not as a string +transformation but as a rule for each output bit: "the `i`-th bit of `f x` is +whatever this decision procedure says". This module turns such a description +into `f ∈ FP`. + +The two inputs are a unary length function — how long the output is, given in +unary so that it is itself a plausible `FP` output — and a bit oracle, a +polynomial-time function reading `pair x (unary i)` and returning the single +bit. The construction is an iteration of an append-one-bit step, run once per +output position, and `iterate_mem_FP` supplies the closure of `FP` under such +iterations. + +This is the bridge that lets a decision procedure written on the RAM surface +(where `RAM_P_eq_P` transfers it to `P`) be used to build a *function* in `FP`, +for which no direct RAM bridge exists. + +## Main definitions + +- `Complexity.bitStep` — the append-one-bit step + +## Main results + +- `Complexity.bitwise_mem_FP` — a bitwise description puts the function in `FP` +-/ + +@[expose] public section + +namespace Complexity + +open Cobham + +/-- One step of the construction: consult the oracle at the current output +length and append the bit it returns. The state is `pair (output so far) input`. +-/ +def bitStep (G : List Bool → List Bool) (z : List Bool) : List Bool := + pair (pairFst z ++ G (pair (pairSnd z) (List.replicate (pairFst z).length true))) + (pairSnd z) + +theorem bitStep_mem_FP {G : List Bool → List Bool} (hG : G ∈ FP) : bitStep G ∈ FP := by + have hfst : (fun z : List Bool => pairFst z) ∈ FP := fstBlock_mem_FP + have hsnd : (fun z : List Bool => pairSnd z) ∈ FP := sndBlock_mem_FP + have hcnt : (fun z : List Bool => List.replicate (pairFst z).length true) ∈ FP := by + have := mem_FP_comp hfst unaryLength_mem_FP + simpa using this + have hquery : (fun z : List Bool => + G (pair (pairSnd z) (List.replicate (pairFst z).length true))) ∈ FP := by + have := mem_FP_comp (pairFn_mem_FP hsnd hcnt) hG + simpa using this + exact pairFn_mem_FP (appendFn_mem_FP hfst hquery) hsnd + +/-- Running the step from the empty output builds the first `n` bits. -/ +theorem bitStep_iterate {G : List Bool → List Bool} {b : List Bool → ℕ → Bool} + (hGspec : ∀ x i, G (pair x (List.replicate i true)) = [b x i]) (x : List Bool) : + ∀ n : ℕ, (bitStep G)^[n] (pair [] x) = pair ((List.range n).map (b x)) x := by + intro n + induction n with + | zero => simp + | succ n ih => + rw [Function.iterate_succ_apply', ih, bitStep, pairFst_pair, pairSnd_pair, + List.length_map, List.length_range, hGspec, List.range_succ, List.map_append] + simp + +/-- **A function described bit by bit is polynomial time.** If the output length +is computable in unary and each output bit is computable from the input and the +position in unary, the function itself is in `FP`. -/ +theorem bitwise_mem_FP {len : List Bool → ℕ} {b : List Bool → ℕ → Bool} + (hlen : (fun x => List.replicate (len x) true) ∈ FP) + {G : List Bool → List Bool} (hG : G ∈ FP) + (hGspec : ∀ x i, G (pair x (List.replicate i true)) = [b x i]) : + (fun x => (List.range (len x)).map (b x)) ∈ FP := by + have hinit : (fun x : List Bool => pair [] x) ∈ FP := + mem_FP_pairWithInput (constFn_mem_FP []) + have hwidth : (fun x : List Bool => pair (List.replicate (len x) true) x) ∈ FP := + mem_FP_pairWithInput hlen + have hbound : ∀ z : List Bool, ∀ n ≤ (List.replicate (len z) true).length, + ((bitStep G)^[n] (pair [] z)).length + ≤ (pair (List.replicate (len z) true) z).length := by + intro z n hn + rw [List.length_replicate] at hn + rw [bitStep_iterate hGspec, pair_length, pair_length, List.length_map, + List.length_range, List.length_replicate] + omega + have hiter := iterate_mem_FP (bitStep_mem_FP hG) hinit hlen hwidth hbound + have := mem_FP_comp hiter fstBlock_mem_FP + refine mem_FP_of_eq this ?_ + intro x + rw [Function.comp_apply, List.length_replicate, bitStep_iterate hGspec, pairFst_pair] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/BoundedQuant.lean b/Complexitylib/Classes/PCP/Internal/BoundedQuant.lean new file mode 100644 index 00000000..8a0b679d --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/BoundedQuant.lean @@ -0,0 +1,72 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.PVerdict +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.P.DecisionFn +public import Complexitylib.Classes.PCP.Internal.BitwiseFP +public import Complexitylib.Classes.Containments + +/-! +# Checking polynomially many conditions + +An algorithm that has to verify a condition at every one of polynomially many +places is a loop, and a loop of polynomial length is still polynomial time. This +module states that as closure of `P` under quantification over an index bounded +by a polynomial-time unary length function. + +The proof reuses the bit-by-bit construction: run the condition at each index, +collect the verdicts into a string, and compare it against the all-ones string +of the same length. The comparison is what turns a list of verdicts into a +single one, and it costs nothing beyond the string equality test already in the +toolkit. + +## Main results + +- `Complexity.forall_unary_mem_P` — a bounded conjunction of `P` conditions +-/ + +@[expose] public section + +namespace Complexity + +/-- **A bounded conjunction of polynomial-time conditions is polynomial time.** +The index runs over `0, …, len x - 1` and is passed to the condition in unary. -/ +theorem forall_unary_mem_P {L : Language} (hL : L ∈ P) {len : List Bool → ℕ} + (hlen : (fun x => List.replicate (len x) true) ∈ FP) : + {x : List Bool | ∀ i < len x, pair x (List.replicate i true) ∈ L} ∈ P := by + obtain ⟨g, hgFP, hg⟩ := exists_decisionFn_of_mem_P hL + have hmap : (fun x => (List.range (len x)).map + (fun i => g (pair x (List.replicate i true)))) ∈ FP := + bitwise_mem_FP hlen hgFP fun _ _ => rfl + refine mem_P_of_decisionFn (eqFlagFn_mem_FP hmap hlen) fun x => ?_ + simp only [Set.mem_setOf_eq] + set a := (List.range (len x)).map (fun i => g (pair x (List.replicate i true))) with ha + set b := List.replicate (len x) true with hb + have hiff : a = b ↔ ∀ i < len x, pair x (List.replicate i true) ∈ L := by + rw [ha, hb, List.eq_replicate_iff] + constructor + · rintro ⟨-, hall⟩ i hi + refine (hg _).2 (hall _ ?_) + exact List.mem_map.2 ⟨i, List.mem_range.2 hi, rfl⟩ + · intro hall + refine ⟨by simp, ?_⟩ + rintro c hc + obtain ⟨i, hi, rfl⟩ := List.mem_map.1 hc + exact (hg _).1 (hall i (List.mem_range.1 hi)) + rw [← hiff] + constructor + · intro hab + rw [(Cobham.eqFlag_eq_true_iff a b).mpr hab] + exact ⟨true, by simp, rfl⟩ + · rintro ⟨c, hc, rfl⟩ + rcases Cobham.eqFlag_flag a b with h | h + · exact (Cobham.eqFlag_eq_true_iff a b).mp h + · rw [h] at hc + simp at hc + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/CNFCount.lean b/Complexitylib/Classes/PCP/Internal/CNFCount.lean new file mode 100644 index 00000000..9be52aad --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/CNFCount.lean @@ -0,0 +1,195 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.HeadOps +public import Complexitylib.Classes.P +public import Complexitylib.Classes.PCP.Internal.UnaryDivMod +public import Complexitylib.Classes.P.Cobham.Internal.PolyRuler + +/-! +# Counting the clauses of an encoded formula + +A formula's encoding is a stream of two-bit tokens, one of which marks the end +of a clause. Counting those tokens counts the clauses, and an algorithm that has +to loop over the clauses needs that count in unary. + +The scan consumes two bits per step, so it is an iteration rather than a fold: +the state is the count so far paired with the unread suffix. + +## Main definitions + +- `Complexity.sepCount` — how many clause markers a bit string carries +- `Complexity.ccStep` — one two-bit step + +## Main results + +- `Complexity.ccStep_iterate` — the scan counts +- `Complexity.clauseCountFn_mem_FP`, `Complexity.clauseCountFn_eq` — counting is + polynomial time +-/ + +@[expose] public section + +namespace Complexity + +/-- How many clause markers — the token `10` — a bit string carries. -/ +def sepCount : List Bool → ℕ + | true :: false :: r => sepCount r + 1 + | _ :: _ :: r => sepCount r + | _ => 0 + +@[simp] theorem sepCount_nil : sepCount [] = 0 := rfl + +theorem sepCount_cons₂ (b0 b1 : Bool) (r : List Bool) : + sepCount (b0 :: b1 :: r) = if b0 = true ∧ b1 = false then sepCount r + 1 else sepCount r := by + cases b0 <;> cases b1 <;> simp [sepCount] + +theorem selectHead_cons (b : Bool) (t x y : List Bool) : + Cobham.selectHead (b :: t) x y = if b then x else y := by + cases b <;> simp [Cobham.selectHead] + +/-- One step: read the next token, and count it if it marks a clause. The state +is `pair count unread`. -/ +def ccStep (z : List Bool) : List Bool := + pair + (Cobham.selectHead (emptyFlag (pairSnd z)) (pairFst z) + (Cobham.selectHead (pairSnd z) + (Cobham.selectHead (dropOne (pairSnd z)) (pairFst z) + (true :: pairFst z)) + (pairFst z))) + (dropOne (dropOne (pairSnd z))) + +theorem ccStep_mem_FP : ccStep ∈ FP := by + have hc : (fun z : List Bool => pairFst z) ∈ FP := Cobham.fstBlock_mem_FP + have hs : (fun z : List Bool => pairSnd z) ∈ FP := Cobham.sndBlock_mem_FP + have hd : (fun z : List Bool => dropOne (pairSnd z)) ∈ FP := dropOneFn_mem_FP hs + refine Cobham.pairFn_mem_FP ?_ (dropOneFn_mem_FP hd) + refine Cobham.selectHeadFn_mem_FP (emptyFlagFn_mem_FP hs) hc ?_ + exact Cobham.selectHeadFn_mem_FP hs + (Cobham.selectHeadFn_mem_FP hd hc (mem_FP_comp hc (Cobham.cons_mem_FP true))) hc + +@[simp] theorem ccStep_nil (c : List Bool) : ccStep (pair c []) = pair c [] := by + rw [ccStep, pairFst_pair, pairSnd_pair, emptyFlag_nil, + selectHead_cons_true] + rfl + +theorem ccStep_cons₂ (c : List Bool) (b0 b1 : Bool) (r : List Bool) : + ccStep (pair c (b0 :: b1 :: r)) + = pair (if b0 = true ∧ b1 = false then true :: c else c) r := by + rw [ccStep, pairFst_pair, pairSnd_pair, emptyFlag_cons, + selectHead_cons_false] + cases b0 + · rw [selectHead_cons] + simp [dropOne] + · cases b1 + · rw [selectHead_cons] + simp only [dropOne, List.drop_succ_cons, List.drop_zero, if_pos] + rw [selectHead_cons] + simp + · rw [selectHead_cons] + simp only [dropOne, List.drop_succ_cons, List.drop_zero, if_pos] + rw [selectHead_cons] + simp + +theorem replicate_true_append_cons (n : ℕ) (c : List Bool) : + List.replicate n true ++ true :: c = true :: (List.replicate n true ++ c) := by + induction n with + | zero => simp + | succ n ih => simp only [List.replicate_succ, List.cons_append, ih] + +/-- **The scan counts.** -/ +theorem ccStep_iterate : ∀ (k : ℕ) (c s : List Bool), s.length ≤ 2 * k → Even s.length → + ccStep^[k] (pair c s) = pair (List.replicate (sepCount s) true ++ c) [] := by + intro k + induction k with + | zero => + intro c s hs _ + have : s = [] := List.eq_nil_of_length_eq_zero (by omega) + subst this + simp + | succ k ih => + intro c s hs hev + rw [Function.iterate_succ_apply] + match s with + | [] => rw [ccStep_nil, ih c [] (by simp) (by simp)] + | [b] => exact absurd hev (by simp [Nat.even_add_one]) + | b0 :: b1 :: r => + rw [ccStep_cons₂] + have hr : r.length ≤ 2 * k := by + simp only [List.length_cons] at hs + omega + have hrev : Even r.length := by + simp only [List.length_cons] at hev + rcases hev with ⟨m, hm⟩ + exact ⟨m - 1, by omega⟩ + rw [ih _ r hr hrev, sepCount_cons₂] + by_cases hcase : b0 = true ∧ b1 = false + · rw [if_pos hcase, if_pos hcase, List.replicate_succ, + List.cons_append, ← replicate_true_append_cons] + · rw [if_neg hcase, if_neg hcase] + +/-! ### The scan as one function -/ + +theorem ccStep_one (c s : List Bool) : + ∃ X Y, ccStep (pair c s) = pair X Y + ∧ X.length ≤ c.length + 1 ∧ Y.length ≤ s.length := by + rw [ccStep, pairFst_pair, pairSnd_pair] + refine ⟨_, _, rfl, ?_, ?_⟩ + · refine le_trans (length_selectHead_le _ _ _) ?_ + simp only [max_le_iff] + refine ⟨by omega, ?_⟩ + refine le_trans (length_selectHead_le _ _ _) ?_ + simp only [max_le_iff] + refine ⟨?_, by omega⟩ + refine le_trans (length_selectHead_le _ _ _) ?_ + simp + · rw [dropOne, dropOne, List.length_drop, List.length_drop] + omega + +theorem ccStep_shape : ∀ (k : ℕ) (c s : List Bool), + ∃ c' s', ccStep^[k] (pair c s) = pair c' s' + ∧ c'.length ≤ c.length + k ∧ s'.length ≤ s.length := by + intro k + induction k with + | zero => intro c s; exact ⟨c, s, rfl, by omega, le_refl _⟩ + | succ k ih => + intro c s + rw [Function.iterate_succ_apply] + obtain ⟨X, Y, hXY, hX, hY⟩ := ccStep_one c s + rw [hXY] + obtain ⟨c', s', h1, h2, h3⟩ := ih X Y + exact ⟨c', s', h1, by omega, by omega⟩ + +/-- **The clause count**, in unary. -/ +noncomputable def clauseCountFn (z : List Bool) : List Bool := + pairFst (ccStep^[z.length] (pair [] z)) + +theorem clauseCountFn_mem_FP : clauseCountFn ∈ FP := by + have hinit : (fun z : List Bool => pair [] z) ∈ FP := + mem_FP_pairWithInput (constFn_mem_FP []) + have hwidth : (fun z : List Bool => polyRuler (Polynomial.C 3 * Polynomial.X + + Polynomial.C 2) (id z)) ∈ FP := + polyRulerFn_mem_FP _ id_mem_FP + have hbound : ∀ z : List Bool, ∀ k ≤ z.length, + (ccStep^[k] (pair [] z)).length + ≤ (polyRuler (Polynomial.C 3 * Polynomial.X + Polynomial.C 2) (id z)).length := by + intro z k hk + obtain ⟨c', s', h1, h2, h3⟩ := ccStep_shape k [] z + rw [h1, pair_length, polyRuler_length] + simp only [Polynomial.eval_add, Polynomial.eval_mul, Polynomial.eval_C, + Polynomial.eval_X, id] + simp only [List.length_nil, Nat.zero_add] at h2 + omega + have hiter := Cobham.iterate_mem_FP ccStep_mem_FP hinit id_mem_FP hwidth hbound + have := mem_FP_comp hiter Cobham.fstBlock_mem_FP + simpa using this + +theorem clauseCountFn_eq {z : List Bool} (h : Even z.length) : + clauseCountFn z = List.replicate (sepCount z) true := by + rw [clauseCountFn, ccStep_iterate z.length [] z (by omega) h, pairFst_pair] + simp + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/CNFMaxVar.lean b/Complexitylib/Classes/PCP/Internal/CNFMaxVar.lean new file mode 100644 index 00000000..18a43a30 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/CNFMaxVar.lean @@ -0,0 +1,160 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.PCP.Internal.CNFTokens +public import Complexitylib.SAT.ThreeCNF +public import Complexitylib.Classes.PCP.Internal.MaxLoop + +/-! +# The largest variable index, by slot + +`CNF.maxVar` folds over clauses and then over literals. An algorithm instead +loops over a flat slot index and takes the largest value it sees. The two are +the same number, and this module says why: each bounds the family of variable +indices and each is attained by it. + +## Main results + +- `Complexity.var_le_maxVar` — every literal's index is at most `maxVar` +- `Complexity.exists_slot_eq_maxVar` — and some literal attains it +- `Complexity.maxOver_slotVar` — the loop computes `maxVar` +-/ + +@[expose] public section + +namespace Complexity + +open SAT + +theorem clause_maxVar_le : ∀ (φ : CNF) {c : Clause}, c ∈ φ → c.maxVar ≤ φ.maxVar := by + intro φ + induction φ with + | nil => intro c hc; simp at hc + | cons c' cs ih => + intro c hc + rw [CNF.maxVar] + rcases List.mem_cons.mp hc with h | h + · rw [h] + exact le_max_left _ _ + · exact le_trans (ih h) (le_max_right _ _) + +/-- Every literal's index is at most the formula's largest. -/ +theorem var_le_maxVar (φ : CNF) {j : ℕ} (hj : j < φ.length) {p : ℕ} + (hp : p < (φ[j]'hj).length) : ((φ[j]'hj)[p]'hp).var ≤ φ.maxVar := + le_trans (Clause.var_le_maxVar (List.getElem_mem hp)) + (clause_maxVar_le φ (List.getElem_mem hj)) + +/-- Some literal of a nonempty clause attains its largest index. -/ +theorem exists_lit_eq_maxVar : ∀ (c : Clause), 0 < c.length → + ∃ p, ∃ hp : p < c.length, ((c[p]'hp).var) = c.maxVar := by + intro c + induction c with + | nil => intro h; simp at h + | cons l ls ih => + intro _ + rcases Nat.eq_zero_or_pos ls.length with hz | hpos + · have hnil : ls = [] := List.eq_nil_of_length_eq_zero hz + subst hnil + exact ⟨0, by simp, by simp⟩ + · obtain ⟨p, hp, hval⟩ := ih hpos + rcases Nat.lt_or_ge (Clause.maxVar ls) l.var with h | h + · refine ⟨0, by simp, ?_⟩ + rw [Clause.maxVar_cons, max_eq_left (le_of_lt h)] + simp + · refine ⟨p + 1, by simp; omega, ?_⟩ + rw [Clause.maxVar_cons, max_eq_right h, ← hval] + simp + +/-- Some clause of a nonempty formula attains its largest index. -/ +theorem exists_clause_eq_maxVar : ∀ (φ : CNF), 0 < φ.length → + ∃ j, ∃ hj : j < φ.length, (φ[j]'hj).maxVar = φ.maxVar := by + intro φ + induction φ with + | nil => intro h; simp at h + | cons c cs ih => + intro _ + rcases Nat.eq_zero_or_pos cs.length with hz | hpos + · have hnil : cs = [] := List.eq_nil_of_length_eq_zero hz + subst hnil + exact ⟨0, by simp, by simp [CNF.maxVar]⟩ + · obtain ⟨j, hj, hval⟩ := ih hpos + rcases Nat.lt_or_ge (CNF.maxVar cs) c.maxVar with h | h + · refine ⟨0, by simp, ?_⟩ + rw [CNF.maxVar, max_eq_left (le_of_lt h)] + simp + · refine ⟨j + 1, by simp; omega, ?_⟩ + rw [CNF.maxVar, max_eq_right h, ← hval] + simp + +/-- **Some literal attains the formula's largest index.** -/ +theorem exists_slot_eq_maxVar (φ : CNF) (h3 : CNF.Is3CNF φ) (h : 0 < φ.length) : + ∃ j, ∃ hj : j < φ.length, ∃ p, ∃ hp : p < (φ[j]'hj).length, + ((φ[j]'hj)[p]'hp).var = φ.maxVar := by + obtain ⟨j, hj, hval⟩ := exists_clause_eq_maxVar φ h + have hlen : (φ[j]'hj).length = 3 := h3 _ (List.getElem_mem hj) + obtain ⟨p, hp, hval'⟩ := exists_lit_eq_maxVar (φ[j]'hj) (by omega) + exact ⟨j, hj, p, hp, by rw [hval', hval]⟩ + +/-! ### The loop computes `maxVar` -/ + +/-- The variable index at a flat slot, read off the encoding. -/ +noncomputable def slotVar (w : List Bool) : List Bool := + litVarFn (pair (pair (divFn [false, false, false] (pairSnd w)) + (modFn [false, false, false] (pairSnd w))) (pairFst w)) + +theorem slotVar_mem_FP : slotVar ∈ FP := by + have hs : (fun w : List Bool => pairSnd w) ∈ FP := Cobham.sndBlock_mem_FP + have hf : (fun w : List Bool => pairFst w) ∈ FP := Cobham.fstBlock_mem_FP + have hd : (fun w : List Bool => divFn [false, false, false] (pairSnd w)) ∈ FP := by + have := mem_FP_comp hs (divFn_mem_FP [false, false, false]) + refine mem_FP_of_eq this fun w => ?_ + rw [Function.comp_apply] + have hm : (fun w : List Bool => modFn [false, false, false] (pairSnd w)) ∈ FP := by + have := mem_FP_comp hs (modFn_mem_FP [false, false, false]) + refine mem_FP_of_eq this fun w => ?_ + rw [Function.comp_apply] + have := mem_FP_comp (Cobham.pairFn_mem_FP (Cobham.pairFn_mem_FP hd hm) hf) litVarFn_mem_FP + refine mem_FP_of_eq this fun w => ?_ + rw [Function.comp_apply, slotVar] + +theorem slotVar_eq (φ : CNF) {i j p : ℕ} (hj : j < φ.length) (hp : p < (φ[j]'hj).length) + (hdj : i / 3 = j) (hdp : i % 3 = p) : + (slotVar (pair φ.encode (List.replicate i true))).length = ((φ[j]'hj)[p]'hp).var := by + rw [slotVar, pairSnd_pair, pairFst_pair, + divFn_eq (by simp) (List.replicate i true), modFn_eq (by simp) (List.replicate i true)] + simp only [List.length_replicate, + show ([false, false, false] : List Bool).length = 3 from rfl, hdj, hdp] + rw [litVarFn_encode φ hj hp, List.length_replicate] + +/-- **The loop computes `maxVar`.** -/ +theorem maxOver_slotVar (φ : CNF) (h3 : CNF.Is3CNF φ) : + maxOver slotVar φ.encode (3 * φ.length) = φ.maxVar := by + rcases Nat.eq_zero_or_pos φ.length with hz | hpos + · have hnil : φ = [] := List.eq_nil_of_length_eq_zero hz + subst hnil + simp [maxOver] + · refine Nat.le_antisymm ?_ ?_ + · refine maxOver_le _ fun i hi => ?_ + have hj : i / 3 < φ.length := by omega + have hp : i % 3 < (φ[i / 3]'hj).length := by + rw [h3 _ (List.getElem_mem hj)] + omega + rw [slotVar_eq φ hj hp rfl rfl] + exact var_le_maxVar φ _ _ + · obtain ⟨j, hj, p, hp, hval⟩ := exists_slot_eq_maxVar φ h3 hpos + have hp3 : p < 3 := by + rw [h3 _ (List.getElem_mem hj)] at hp + exact hp + have hi : 3 * j + p < 3 * φ.length := by omega + have hdiv : (3 * j + p) / 3 = j := by omega + have hmod : (3 * j + p) % 3 = p := by omega + have hstep := le_maxOver (f := slotVar) (z := φ.encode) (3 * φ.length) (3 * j + p) hi + rw [slotVar_eq φ hj hp hdiv hmod] at hstep + rw [← hval] + exact hstep + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/CNFSegment.lean b/Complexitylib/Classes/PCP/Internal/CNFSegment.lean new file mode 100644 index 00000000..037e90c7 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/CNFSegment.lean @@ -0,0 +1,398 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.PolyRuler +public import Complexitylib.Classes.P.Cobham.Internal.HeadOps +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.PCP.Internal.CNFCount +public import Complexitylib.Classes.PCP.Internal.PositionsFP + +/-! +# Cutting out one clause of an encoded formula + +Reading the `j`-th clause of an encoded formula is the same two-bit scan that +counts the clauses, with two additions: a target index to compare against, and a +buffer that collects tokens while the count matches. + +The state is `pair (pair target count) (pair collected unread)`. + +## Main definitions + +- `Complexity.segStep` — one two-bit step of the extraction +- `Complexity.segFrom` — the tokens of one segment + +## Main results + +- `Complexity.segStep_mem_FP` — the extraction is polynomial time +- `Complexity.segAtFn_mem_FP`, `Complexity.segAtFn_eq` — the packaged + extraction +- `Complexity.litSegFn_eq` — the `p`-th literal of the `j`-th clause +-/ + +@[expose] public section + +namespace Complexity + +/-- The tokens of the segment with index `t`, having already passed `c` +separators, where the separator is the token `s0 s1`. -/ +def segFrom (s0 s1 : Bool) (t c : ℕ) : List Bool → List Bool + | b0 :: b1 :: r => + if b0 = s0 ∧ b1 = s1 then segFrom s0 s1 t (c + 1) r + else if c = t then b0 :: b1 :: segFrom s0 s1 t c r else segFrom s0 s1 t c r + | _ => [] + +@[simp] theorem segFrom_nil (s0 s1 : Bool) (t c : ℕ) : segFrom s0 s1 t c [] = [] := rfl + +theorem segFrom_cons₂ (s0 s1 : Bool) (t c : ℕ) (b0 b1 : Bool) (r : List Bool) : + segFrom s0 s1 t c (b0 :: b1 :: r) + = if b0 = s0 ∧ b1 = s1 then segFrom s0 s1 t (c + 1) r + else if c = t then b0 :: b1 :: segFrom s0 s1 t c r else segFrom s0 s1 t c r := rfl + +/-- The target index carried by the state. -/ +def segTgt (z : List Bool) : List Bool := pairFst (pairFst z) + +/-- The number of separators already passed. -/ +def segCnt (z : List Bool) : List Bool := pairSnd (pairFst z) + +/-- The tokens collected so far. -/ +def segColl (z : List Bool) : List Bool := pairFst (pairSnd z) + +/-- The unread suffix. -/ +def segRest (z : List Bool) : List Bool := pairSnd (pairSnd z) + +/-- Does the string begin with the bit `b`, as a flag? -/ +def matchBit (b : Bool) (s : List Bool) : List Bool := + if b then Cobham.selectHead s [true] [false] else Cobham.selectHead s [false] [true] + +theorem matchBit_mem_FP (b : Bool) {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => matchBit b (a z)) ∈ FP := by + cases b + · exact Cobham.selectHeadFn_mem_FP ha (constFn_mem_FP [false]) (constFn_mem_FP [true]) + · exact Cobham.selectHeadFn_mem_FP ha (constFn_mem_FP [true]) (constFn_mem_FP [false]) + +theorem matchBit_cons (b c : Bool) (t : List Bool) : + matchBit b (c :: t) = if c = b then [true] else [false] := by + cases b <;> cases c <;> simp [matchBit, selectHead_cons] + +/-- Is this token the separator? -/ +def segIsSep (s0 s1 : Bool) (z : List Bool) : List Bool := + andBit (matchBit s0 (segRest z)) (matchBit s1 (dropOne (segRest z))) + +/-- Are we inside the segment we want? -/ +def segHere (z : List Bool) : List Bool := + Cobham.lenEqFlag (segCnt z) (segTgt z) + +/-- One two-bit step of the extraction. -/ +def segStep (s0 s1 : Bool) (z : List Bool) : List Bool := + Cobham.selectHead (emptyFlag (segRest z)) z + (pair + (pair (segTgt z) + (Cobham.selectHead (segIsSep s0 s1 z) (true :: segCnt z) (segCnt z))) + (pair + (Cobham.selectHead (segIsSep s0 s1 z) (segColl z) + (Cobham.selectHead (segHere z) + (segColl z ++ (segRest z).take 2) (segColl z))) + (dropOne (dropOne (segRest z))))) + +theorem segTgt_mem_FP : segTgt ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + +theorem segCnt_mem_FP : segCnt ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + +theorem segColl_mem_FP : segColl ∈ FP := + mem_FP_comp Cobham.sndBlock_mem_FP Cobham.fstBlock_mem_FP + +theorem segRest_mem_FP : segRest ∈ FP := + mem_FP_comp Cobham.sndBlock_mem_FP Cobham.sndBlock_mem_FP + +theorem segIsSep_mem_FP (s0 s1 : Bool) : segIsSep s0 s1 ∈ FP := + andBitFn_mem_FP (matchBit_mem_FP s0 segRest_mem_FP) + (matchBit_mem_FP s1 (dropOneFn_mem_FP segRest_mem_FP)) + +theorem segHere_mem_FP : segHere ∈ FP := + andBitFn_mem_FP (lenLeFlagFn_mem_FP segCnt_mem_FP segTgt_mem_FP) + (lenLeFlagFn_mem_FP segTgt_mem_FP segCnt_mem_FP) + +theorem segStep_mem_FP (s0 s1 : Bool) : segStep s0 s1 ∈ FP := by + have htake : (fun z : List Bool => (segRest z).take 2) ∈ FP := by + have := Cobham.takeLenFn_mem_FP (constFn_mem_FP [false, false]) segRest_mem_FP + refine mem_FP_of_eq this fun z => ?_ + rfl + refine Cobham.selectHeadFn_mem_FP (emptyFlagFn_mem_FP segRest_mem_FP) id_mem_FP ?_ + refine Cobham.pairFn_mem_FP (Cobham.pairFn_mem_FP segTgt_mem_FP ?_) + (Cobham.pairFn_mem_FP ?_ (dropOneFn_mem_FP (dropOneFn_mem_FP segRest_mem_FP))) + · exact Cobham.selectHeadFn_mem_FP (segIsSep_mem_FP s0 s1) + (mem_FP_comp segCnt_mem_FP (Cobham.cons_mem_FP true)) segCnt_mem_FP + · refine Cobham.selectHeadFn_mem_FP (segIsSep_mem_FP s0 s1) segColl_mem_FP ?_ + exact Cobham.selectHeadFn_mem_FP segHere_mem_FP + (Cobham.appendFn_mem_FP segColl_mem_FP htake) segColl_mem_FP + +/-! ### What the scan collects -/ + +variable (s0 s1 : Bool) + +@[simp] theorem segStep_nil (tgt cnt coll : List Bool) : + segStep s0 s1 (pair (pair tgt cnt) (pair coll [])) = pair (pair tgt cnt) (pair coll []) := by + have hr : segRest (pair (pair tgt cnt) (pair coll [])) = [] := by + rw [segRest, pairSnd_pair, pairSnd_pair] + rw [segStep, hr, emptyFlag_nil, selectHead_cons_true] + +theorem segStep_cons₂ (tgt cnt coll : List Bool) (b0 b1 : Bool) (r : List Bool) : + segStep s0 s1 (pair (pair tgt cnt) (pair coll (b0 :: b1 :: r))) + = if b0 = s0 ∧ b1 = s1 then + pair (pair tgt (true :: cnt)) (pair coll r) + else pair (pair tgt cnt) + (pair (if cnt.length = tgt.length then coll ++ [b0, b1] else coll) r) := by + have hr : segRest (pair (pair tgt cnt) (pair coll (b0 :: b1 :: r))) = b0 :: b1 :: r := by + rw [segRest, pairSnd_pair, pairSnd_pair] + have ht : segTgt (pair (pair tgt cnt) (pair coll (b0 :: b1 :: r))) = tgt := by + rw [segTgt, pairFst_pair, pairFst_pair] + have hc : segCnt (pair (pair tgt cnt) (pair coll (b0 :: b1 :: r))) = cnt := by + rw [segCnt, pairFst_pair, pairSnd_pair] + have hl : segColl (pair (pair tgt cnt) (pair coll (b0 :: b1 :: r))) = coll := by + rw [segColl, pairSnd_pair, pairFst_pair] + have hsep : segIsSep s0 s1 (pair (pair tgt cnt) (pair coll (b0 :: b1 :: r))) + = if b0 = s0 ∧ b1 = s1 then [true] else [false] := by + rw [segIsSep, hr] + simp only [dropOne, List.drop_succ_cons, List.drop_zero] + rw [matchBit_cons, matchBit_cons] + by_cases h0 : b0 = s0 <;> by_cases h1 : b1 = s1 <;> simp [h0, h1, andBit] + have hhere : segHere (pair (pair tgt cnt) (pair coll (b0 :: b1 :: r))) + = if cnt.length = tgt.length then [true] else [false] := by + rw [segHere, hc, ht] + by_cases h : cnt.length = tgt.length + · rw [if_pos h, Cobham.lenEqFlag_eq_true_iff] + exact h + · rw [if_neg h] + rcases Cobham.lenEqFlag_flag cnt tgt with hf | hf + · rw [Cobham.lenEqFlag_eq_true_iff] at hf + exact absurd hf h + · exact hf + rw [segStep, hr, hsep, hhere, ht, hc, hl, emptyFlag_cons, selectHead_cons_false] + by_cases hcase : b0 = s0 ∧ b1 = s1 + · rw [if_pos hcase, if_pos hcase, selectHead_cons_true, selectHead_cons_true] + simp [dropOne] + · rw [if_neg hcase, if_neg hcase, selectHead_cons_false, selectHead_cons_false] + by_cases hh : cnt.length = tgt.length + · rw [if_pos hh, if_pos hh, selectHead_cons_true] + simp [dropOne] + · rw [if_neg hh, if_neg hh, selectHead_cons_false] + simp [dropOne] + +/-- **The scan collects the segment.** -/ +theorem segStep_iterate : ∀ (k : ℕ) (tgt cnt coll s : List Bool), + s.length ≤ 2 * k → Even s.length → + segColl ((segStep s0 s1)^[k] (pair (pair tgt cnt) (pair coll s))) + = coll ++ segFrom s0 s1 tgt.length cnt.length s := by + intro k + induction k with + | zero => + intro tgt cnt coll s hs _ + have : s = [] := List.eq_nil_of_length_eq_zero (by omega) + subst this + rw [Function.iterate_zero_apply, segColl, pairSnd_pair, pairFst_pair] + simp + | succ k ih => + intro tgt cnt coll s hs hev + rw [Function.iterate_succ_apply] + match s with + | [] => rw [segStep_nil, ih tgt cnt coll [] (by simp) (by simp)] + | [b] => exact absurd hev (by simp [Nat.even_add_one]) + | b0 :: b1 :: r => + have hr : r.length ≤ 2 * k := by + simp only [List.length_cons] at hs + omega + have hrev : Even r.length := by + simp only [List.length_cons] at hev + rcases hev with ⟨m, hm⟩ + exact ⟨m - 1, by omega⟩ + rw [segStep_cons₂, segFrom_cons₂] + by_cases hcase : b0 = s0 ∧ b1 = s1 + · rw [if_pos hcase, if_pos hcase, ih tgt (true :: cnt) coll r hr hrev] + simp + · rw [if_neg hcase, if_neg hcase] + by_cases hh : cnt.length = tgt.length + · rw [if_pos hh, if_pos hh, ih tgt cnt _ r hr hrev] + simp + · rw [if_neg hh, if_neg hh, ih tgt cnt coll r hr hrev] + +theorem even_length_segFrom (s0 s1 : Bool) (t : ℕ) : + ∀ (n : ℕ) (s : List Bool) (c : ℕ), s.length ≤ n → Even s.length → + Even (segFrom s0 s1 t c s).length := by + intro n + induction n with + | zero => + intro s c hs _ + have : s = [] := List.eq_nil_of_length_eq_zero (by omega) + subst this + simp + | succ n ih => + intro s c hs hev + match s with + | [] => simp + | [b] => exact absurd hev (by simp [Nat.even_add_one]) + | b0 :: b1 :: r => + rw [segFrom_cons₂] + have hr : r.length ≤ n := by + simp only [List.length_cons] at hs + omega + have hrev : Even r.length := by + simp only [List.length_cons] at hev + rcases hev with ⟨m, hm⟩ + exact ⟨m - 1, by omega⟩ + by_cases hcase : b0 = s0 ∧ b1 = s1 + · rw [if_pos hcase] + exact ih r (c + 1) hr hrev + · rw [if_neg hcase] + by_cases hh : c = t + · rw [if_pos hh] + simp only [List.length_cons] + rcases ih r c hr hrev with ⟨m, hm⟩ + exact ⟨m + 1, by omega⟩ + · rw [if_neg hh] + exact ih r c hr hrev + +/-! ### The scan as one function -/ + +theorem segStep_one (tgt cnt coll s : List Bool) : + ∃ cnt' coll' s', segStep s0 s1 (pair (pair tgt cnt) (pair coll s)) + = pair (pair tgt cnt') (pair coll' s') + ∧ cnt'.length ≤ cnt.length + 1 ∧ coll'.length ≤ coll.length + 2 + ∧ s'.length ≤ s.length := by + have hr : segRest (pair (pair tgt cnt) (pair coll s)) = s := by + rw [segRest, pairSnd_pair, pairSnd_pair] + have ht : segTgt (pair (pair tgt cnt) (pair coll s)) = tgt := by + rw [segTgt, pairFst_pair, pairFst_pair] + have hc : segCnt (pair (pair tgt cnt) (pair coll s)) = cnt := by + rw [segCnt, pairFst_pair, pairSnd_pair] + have hl : segColl (pair (pair tgt cnt) (pair coll s)) = coll := by + rw [segColl, pairSnd_pair, pairFst_pair] + match s with + | [] => + exact ⟨cnt, coll, [], segStep_nil s0 s1 tgt cnt coll, by omega, by omega, le_refl _⟩ + | b :: t => + rw [segStep, hr, emptyFlag_cons, selectHead_cons_false, ht, hc, hl] + refine ⟨_, _, _, rfl, ?_, ?_, ?_⟩ + · refine le_trans (length_selectHead_le _ _ _) ?_ + simp + · refine le_trans (length_selectHead_le _ _ _) ?_ + simp only [max_le_iff] + refine ⟨by omega, ?_⟩ + refine le_trans (length_selectHead_le _ _ _) ?_ + simp only [max_le_iff] + refine ⟨?_, by omega⟩ + rw [List.length_append] + have h2 : ((b :: t).take 2).length ≤ 2 := by + rw [List.length_take] + omega + omega + · rw [dropOne, dropOne, List.length_drop, List.length_drop] + omega + +theorem segStep_shape : ∀ (k : ℕ) (tgt cnt coll s : List Bool), + ∃ cnt' coll' s', (segStep s0 s1)^[k] (pair (pair tgt cnt) (pair coll s)) + = pair (pair tgt cnt') (pair coll' s') + ∧ cnt'.length ≤ cnt.length + k ∧ coll'.length ≤ coll.length + 2 * k + ∧ s'.length ≤ s.length := by + intro k + induction k with + | zero => intro tgt cnt coll s; exact ⟨cnt, coll, s, rfl, by omega, by omega, le_refl _⟩ + | succ k ih => + intro tgt cnt coll s + rw [Function.iterate_succ_apply] + obtain ⟨cnt₁, coll₁, s₁, h1, hc1, hl1, hs1⟩ := segStep_one s0 s1 tgt cnt coll s + rw [h1] + obtain ⟨cnt', coll', s', h2, hc2, hl2, hs2⟩ := ih tgt cnt₁ coll₁ s₁ + exact ⟨cnt', coll', s', h2, by omega, by omega, by omega⟩ + +/-- **The packaged extraction**, on `pair (unary index) encoding`. -/ +noncomputable def segAtFn (s0 s1 : Bool) (z : List Bool) : List Bool := + segColl ((segStep s0 s1)^[(pairSnd z).length] + (pair (pair (pairFst z) []) (pair [] (pairSnd z)))) + +theorem segAtFn_mem_FP : segAtFn s0 s1 ∈ FP := by + have hf : (fun z : List Bool => pairFst z) ∈ FP := Cobham.fstBlock_mem_FP + have hs : (fun z : List Bool => pairSnd z) ∈ FP := Cobham.sndBlock_mem_FP + have hinit : (fun z : List Bool => + pair (pair (pairFst z) []) (pair [] (pairSnd z))) ∈ FP := + Cobham.pairFn_mem_FP (Cobham.pairFn_mem_FP hf (constFn_mem_FP [])) + (Cobham.pairFn_mem_FP (constFn_mem_FP []) hs) + have hwidth : (fun z : List Bool => polyRuler (Polynomial.C 11 * Polynomial.X + + Polynomial.C 8) (id z)) ∈ FP := polyRulerFn_mem_FP _ id_mem_FP + have hbound : ∀ z : List Bool, ∀ k ≤ (pairSnd z).length, + ((segStep s0 s1)^[k] + (pair (pair (pairFst z) []) (pair [] (pairSnd z)))).length + ≤ (polyRuler (Polynomial.C 11 * Polynomial.X + Polynomial.C 8) (id z)).length := by + intro z k hk + obtain ⟨cnt', coll', s', h1, hc, hl, hss⟩ := + segStep_shape s0 s1 k (pairFst z) [] [] (pairSnd z) + have hfz : (pairFst z).length ≤ z.length := fstBlock_length_le z + have hsz : (pairSnd z).length ≤ z.length := pairSnd_length_le z + rw [h1, pair_length, pair_length, pair_length, polyRuler_length] + simp only [Polynomial.eval_add, Polynomial.eval_mul, Polynomial.eval_C, + Polynomial.eval_X, id, List.length_nil, Nat.zero_add] at * + omega + have hiter := Cobham.iterate_mem_FP (segStep_mem_FP s0 s1) hinit hs hwidth hbound + have := mem_FP_comp hiter segColl_mem_FP + simpa only using this + +theorem segAtFn_eq {j : ℕ} {e : List Bool} (h : Even e.length) : + segAtFn s0 s1 (pair (List.replicate j true) e) = segFrom s0 s1 j 0 e := by + rw [segAtFn, pairFst_pair, pairSnd_pair, + segStep_iterate s0 s1 e.length _ _ _ e (by omega) h, List.length_replicate, + List.length_nil] + simp + +/-! ### Down to a literal -/ + +/-- **The `p`-th literal of the `j`-th clause**, on +`pair (pair (unary j) (unary p)) encoding`. -/ +noncomputable def litSegFn (z : List Bool) : List Bool := + segAtFn false true + (pair (pairSnd (pairFst z)) + (segAtFn true false (pair (pairFst (pairFst z)) (pairSnd z)))) + +theorem litSegFn_mem_FP : litSegFn ∈ FP := by + have hj : (fun z : List Bool => pairFst (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + have hp : (fun z : List Bool => pairSnd (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + have he : (fun z : List Bool => pairSnd z) ∈ FP := Cobham.sndBlock_mem_FP + have hcl : (fun z : List Bool => segAtFn true false + (pair (pairFst (pairFst z)) (pairSnd z))) ∈ FP := by + have := mem_FP_comp (Cobham.pairFn_mem_FP hj he) (segAtFn_mem_FP true false) + simpa only using this + have := mem_FP_comp (Cobham.pairFn_mem_FP hp hcl) (segAtFn_mem_FP false true) + simpa only using this + +theorem litSegFn_eq {j p : ℕ} {e : List Bool} (h : Even e.length) : + litSegFn (pair (pair (List.replicate j true) (List.replicate p true)) e) + = segFrom false true p 0 (segFrom true false j 0 e) := by + rw [litSegFn, pairFst_pair, pairSnd_pair, pairFst_pair, + pairSnd_pair, segAtFn_eq true false h, + segAtFn_eq false true (even_length_segFrom true false j e.length e 0 (le_refl _) h)] + +/-- The variable a literal names, in unary. -/ +noncomputable def litVarFn (z : List Bool) : List Bool := halfFn ((litSegFn z).drop 2) + +theorem litVarFn_mem_FP : litVarFn ∈ FP := by + have hdrop : (fun z : List Bool => (litSegFn z).drop 2) ∈ FP := by + have := dropLenFn_mem_FP (constFn_mem_FP [false, false]) litSegFn_mem_FP + refine mem_FP_of_eq this fun z => ?_ + rfl + have := mem_FP_comp hdrop halfFn_mem_FP + simpa only using this + +/-- The sign a literal carries, as a flag. -/ +noncomputable def litSignFn (z : List Bool) : List Bool := + Cobham.selectHead (litSegFn z) [true] [false] + +theorem litSignFn_mem_FP : litSignFn ∈ FP := + Cobham.selectHeadFn_mem_FP litSegFn_mem_FP (constFn_mem_FP [true]) (constFn_mem_FP [false]) + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/CNFTokens.lean b/Complexitylib/Classes/PCP/Internal/CNFTokens.lean new file mode 100644 index 00000000..486d0e0b --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/CNFTokens.lean @@ -0,0 +1,319 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.CNFCount +public import Complexitylib.Classes.PCP.Internal.CNFSegment +public import Complexitylib.SAT.Verifier + +/-! +# The scan agrees with the encoding + +The clause counter reads two bits at a time and counts the token `10`. This +module checks that against the encoding it is meant to read: every token +occupies two bits, only the clause separator is `10`, and a formula's encoding +carries one separator per clause. + +Because tokens are two bits wide and the scan steps two bits at a time, a `10` +pattern straddling two tokens is never seen. + +## Main results + +- `Complexity.sepCount_encodeTokens` — the scan counts separators +- `Complexity.sepCount_encode` — a formula's encoding has one per clause +- `Complexity.segFrom_tokenJoin` — the extraction returns one segment +- `Complexity.litSegFn_encode` — the packaged extraction returns one literal +- `Complexity.litVarFn_encode` — and its variable index +-/ + +@[expose] public section + +namespace Complexity + +open SAT + +theorem length_encode_token (t : EncToken) : t.encode.length = 2 := by + cases t with + | bit b => cases b <;> rfl + | litSep => rfl + | clauseSep => rfl + +/-- **The scan counts separators.** -/ +theorem sepCount_encodeTokens : ∀ toks : List EncToken, + sepCount (encodeTokens toks) + = (toks.filter (fun t => t = EncToken.clauseSep)).length := by + intro toks + induction toks with + | nil => rfl + | cons t ts ih => + rw [encodeTokens_cons, List.filter_cons] + cases t + case bit b => + cases b + · show sepCount ([false, false] ++ encodeTokens ts) = _ + rw [show ([false, false] ++ encodeTokens ts) + = false :: false :: encodeTokens ts from rfl, sepCount_cons₂, if_neg (by simp)] + simp [ih] + · show sepCount ([true, true] ++ encodeTokens ts) = _ + rw [show ([true, true] ++ encodeTokens ts) + = true :: true :: encodeTokens ts from rfl, sepCount_cons₂, if_neg (by simp)] + simp [ih] + case litSep => + show sepCount ([false, true] ++ encodeTokens ts) = _ + rw [show ([false, true] ++ encodeTokens ts) + = false :: true :: encodeTokens ts from rfl, sepCount_cons₂, if_neg (by simp)] + simp [ih] + case clauseSep => + show sepCount ([true, false] ++ encodeTokens ts) = _ + rw [show ([true, false] ++ encodeTokens ts) + = true :: false :: encodeTokens ts from rfl, sepCount_cons₂, if_pos (by simp)] + simp [ih] + +theorem clause_tokens_no_sep (c : Clause) : + (Clause.tokens c).filter (fun t => t = EncToken.clauseSep) = [] := by + induction c with + | nil => rfl + | cons l ls ih => + rw [Clause.tokens, List.filter_append, List.filter_append, ih] + have hraw : (Lit.rawTokens l).filter (fun t => t = EncToken.clauseSep) = [] := by + rw [Lit.rawTokens, List.filter_map] + simp + rw [hraw] + rfl + +/-- **A formula's encoding has one separator per clause.** -/ +theorem sepCount_encode (φ : CNF) : sepCount φ.encode = φ.length := by + rw [← CNF.encodeTokens_tokens, sepCount_encodeTokens] + induction φ with + | nil => rfl + | cons c cs ih => + rw [CNF.tokens, List.filter_append, List.filter_append, List.length_append, + List.length_append, clause_tokens_no_sep, ih] + simp + omega + +/-! ### The extraction returns one clause -/ + +theorem hne_clauseSep : ∀ t : EncToken, t ≠ EncToken.clauseSep → + ∃ c0 c1, t.encode = [c0, c1] ∧ ¬(c0 = true ∧ c1 = false) := by + intro t h + cases t with + | bit b => + cases b + · exact ⟨false, false, rfl, by simp⟩ + · exact ⟨true, true, rfl, by simp⟩ + | litSep => exact ⟨false, true, rfl, by simp⟩ + | clauseSep => exact absurd rfl h + +theorem hne_litSep : ∀ t : EncToken, t ≠ EncToken.litSep → + ∃ c0 c1, t.encode = [c0, c1] ∧ ¬(c0 = false ∧ c1 = true) := by + intro t h + cases t with + | bit b => + cases b + · exact ⟨false, false, rfl, by simp⟩ + · exact ⟨true, true, rfl, by simp⟩ + | litSep => exact absurd rfl h + | clauseSep => exact ⟨true, false, rfl, by simp⟩ + +/-- Reading past a run of non-separator tokens collects them, or not, according +to whether the count matches. -/ +theorem segFrom_encodeTokens_noSep {sep : EncToken} {b0 b1 : Bool} + (hne : ∀ t : EncToken, t ≠ sep → ∃ c0 c1, t.encode = [c0, c1] ∧ ¬(c0 = b0 ∧ c1 = b1)) : + ∀ (toks : List EncToken), (∀ t ∈ toks, t ≠ sep) → ∀ (v : List Bool) (t c : ℕ), + segFrom b0 b1 t c (encodeTokens toks ++ v) + = (if c = t then encodeTokens toks else []) ++ segFrom b0 b1 t c v := by + intro toks + induction toks with + | nil => intro _ v t c; by_cases h : c = t <;> simp [h] + | cons tk ts ih => + intro hall v t c + obtain ⟨c0, c1, henc, hnec⟩ := hne tk (hall tk (by simp)) + rw [encodeTokens_cons, henc, List.append_assoc, + show ([c0, c1] ++ (encodeTokens ts ++ v)) = c0 :: c1 :: (encodeTokens ts ++ v) from rfl, + segFrom_cons₂, if_neg hnec, ih (fun t' ht' => hall t' (by simp [ht'])) v t c] + by_cases h : c = t + · rw [if_pos h, if_pos h, if_pos h] + rfl + · rw [if_neg h, if_neg h, if_neg h] + +theorem segFrom_of_gt (s0 s1 : Bool) : ∀ (n : ℕ) (s : List Bool) (t c : ℕ), + s.length ≤ n → t < c → segFrom s0 s1 t c s = [] := by + intro n + induction n with + | zero => + intro s t c hs _ + have : s = [] := List.eq_nil_of_length_eq_zero (by omega) + subst this + rfl + | succ n ih => + intro s t c hs hlt + match s with + | [] => rfl + | [b] => rfl + | b0 :: b1 :: r => + have hr : r.length ≤ n := by + simp only [List.length_cons] at hs + omega + rw [segFrom_cons₂] + by_cases hcase : b0 = s0 ∧ b1 = s1 + · rw [if_pos hcase] + exact ih r t (c + 1) hr (by omega) + · rw [if_neg hcase, if_neg (by omega : ¬ c = t)] + exact ih r t c hr hlt + +/-- Token segments joined by a separator. -/ +def tokenJoin (sep : EncToken) : List (List EncToken) → List EncToken + | [] => [] + | g :: gs => g ++ [sep] ++ tokenJoin sep gs + +theorem cnf_tokens_eq (φ : CNF) : + CNF.tokens φ = tokenJoin EncToken.clauseSep (φ.map Clause.tokens) := by + induction φ with + | nil => rfl + | cons c cs ih => rw [CNF.tokens, ih, List.map_cons, tokenJoin] + +theorem clause_tokens_eq (c : Clause) : + Clause.tokens c = tokenJoin EncToken.litSep (c.map Lit.rawTokens) := by + induction c with + | nil => rfl + | cons l ls ih => rw [Clause.tokens, ih, List.map_cons, tokenJoin] + +/-- **The extraction returns one segment.** -/ +theorem segFrom_tokenJoin {sep : EncToken} {b0 b1 : Bool} (hsep : sep.encode = [b0, b1]) + (hne : ∀ t : EncToken, t ≠ sep → ∃ c0 c1, t.encode = [c0, c1] ∧ ¬(c0 = b0 ∧ c1 = b1)) : + ∀ (segs : List (List EncToken)), (∀ g ∈ segs, ∀ t ∈ g, t ≠ sep) → + ∀ (t c : ℕ), c ≤ t → ∀ hlt : t - c < segs.length, + segFrom b0 b1 t c (encodeTokens (tokenJoin sep segs)) + = encodeTokens (segs[t - c]'hlt) := by + intro segs + induction segs with + | nil => intro _ t c _ hlt; simp at hlt + | cons g gs ih => + intro hall t c hle hlt + have hnosep : ∀ t' ∈ g, t' ≠ sep := hall g (by simp) + have hsplit : tokenJoin sep (g :: gs) = g ++ ([sep] ++ tokenJoin sep gs) := by + rw [tokenJoin, List.append_assoc] + rw [hsplit, encodeTokens_append] + rw [show segFrom b0 b1 t c (encodeTokens g ++ + encodeTokens ([sep] ++ tokenJoin sep gs)) + = (if c = t then encodeTokens g else []) + ++ segFrom b0 b1 t c (encodeTokens ([sep] ++ tokenJoin sep gs)) from + segFrom_encodeTokens_noSep hne g hnosep _ t c] + have hs : encodeTokens ([sep] ++ tokenJoin sep gs) + = b0 :: b1 :: encodeTokens (tokenJoin sep gs) := by + rw [encodeTokens_append, + show encodeTokens [sep] = sep.encode from by simp, hsep] + rfl + rw [hs, segFrom_cons₂, if_pos (by simp : (b0 = b0 ∧ b1 = b1))] + rcases Nat.eq_or_lt_of_le hle with heq | hlt2 + · rw [if_pos heq, segFrom_of_gt b0 b1 + (encodeTokens (tokenJoin sep gs)).length _ t (c + 1) (le_refl _) (by omega)] + have hzero : t - c = 0 := by omega + simp only [hzero] + simp + · rw [if_neg (by omega : ¬ c = t)] + have hidx : t - c = (t - (c + 1)) + 1 := by omega + have hlt' : t - (c + 1) < gs.length := by + rw [hidx] at hlt + simp only [List.length_cons] at hlt + omega + rw [ih (fun g' hg' => hall g' (by simp [hg'])) t (c + 1) (by omega) hlt'] + simp only [List.nil_append] + congr 1 + simp only [hidx] + simp + +/-! ### Down to a literal of a real formula -/ + +theorem length_encodeTokens : ∀ toks : List EncToken, + (encodeTokens toks).length = 2 * toks.length := by + intro toks + induction toks with + | nil => rfl + | cons t ts ih => + rw [encodeTokens_cons, List.length_append, length_encode_token, ih, + List.length_cons] + ring + +theorem even_length_encode (φ : CNF) : Even φ.encode.length := by + rw [← CNF.encodeTokens_tokens, length_encodeTokens] + exact ⟨(CNF.tokens φ).length, by ring⟩ + +theorem rawTokens_no_litSep (l : Lit) : + ∀ t ∈ Lit.rawTokens l, t ≠ EncToken.litSep := by + intro t ht + rw [Lit.rawTokens, List.mem_map] at ht + obtain ⟨b, -, hb⟩ := ht + rw [← hb] + simp + +/-- **The extraction returns one clause of a real formula.** -/ +theorem segFrom_encode_clause (φ : CNF) {j : ℕ} (hj : j < φ.length) : + segFrom true false j 0 φ.encode = encodeTokens (Clause.tokens (φ[j]'hj)) := by + rw [← CNF.encodeTokens_tokens, cnf_tokens_eq] + have hall : ∀ g ∈ φ.map Clause.tokens, ∀ t ∈ g, t ≠ EncToken.clauseSep := by + intro g hg t ht hcon + rw [List.mem_map] at hg + obtain ⟨cl, -, hcl⟩ := hg + rw [← hcl] at ht + have hmem : t ∈ (Clause.tokens cl).filter (fun t => t = EncToken.clauseSep) := by + rw [List.mem_filter] + exact ⟨ht, by simp [hcon]⟩ + rw [clause_tokens_no_sep cl] at hmem + simp at hmem + have hlt : j - 0 < (φ.map Clause.tokens).length := by + rw [List.length_map] + omega + rw [segFrom_tokenJoin rfl hne_clauseSep _ hall j 0 (Nat.zero_le _) hlt] + congr 1 + simp + +/-- **The extraction returns one literal of a real clause.** -/ +theorem segFrom_encode_lit (cl : Clause) {p : ℕ} (hp : p < cl.length) : + segFrom false true p 0 (encodeTokens (Clause.tokens cl)) + = encodeTokens (Lit.rawTokens (cl[p]'hp)) := by + rw [clause_tokens_eq] + have hall : ∀ g ∈ cl.map Lit.rawTokens, ∀ t ∈ g, t ≠ EncToken.litSep := by + intro g hg t ht + rw [List.mem_map] at hg + obtain ⟨l, -, hl⟩ := hg + rw [← hl] at ht + exact rawTokens_no_litSep l t ht + have hlt : p - 0 < (cl.map Lit.rawTokens).length := by + rw [List.length_map] + omega + rw [segFrom_tokenJoin rfl hne_litSep _ hall p 0 (Nat.zero_le _) hlt] + congr 1 + simp + +/-- **The packaged extraction returns one literal.** -/ +theorem litSegFn_encode (φ : CNF) {j p : ℕ} (hj : j < φ.length) + (hp : p < (φ[j]'hj).length) : + litSegFn (pair (pair (List.replicate j true) (List.replicate p true)) φ.encode) + = encodeTokens (Lit.rawTokens ((φ[j]'hj)[p]'hp)) := by + rw [litSegFn_eq (even_length_encode φ), segFrom_encode_clause φ hj, + segFrom_encode_lit _ hp] + +theorem length_rawTokens (l : Lit) : (Lit.rawTokens l).length = l.var + 1 := by + rw [Lit.rawTokens, List.length_map, Lit.encodeRaw_length] + +/-- **The extracted literal has the length its variable index dictates.** -/ +theorem length_litSegFn_encode (φ : CNF) {j p : ℕ} (hj : j < φ.length) + (hp : p < (φ[j]'hj).length) : + (litSegFn (pair (pair (List.replicate j true) (List.replicate p true)) + φ.encode)).length = 2 * (((φ[j]'hj)[p]'hp).var + 1) := by + rw [litSegFn_encode φ hj hp, length_encodeTokens, length_rawTokens] + +/-- **The extraction reads off the variable index.** -/ +theorem litVarFn_encode (φ : CNF) {j p : ℕ} (hj : j < φ.length) + (hp : p < (φ[j]'hj).length) : + litVarFn (pair (pair (List.replicate j true) (List.replicate p true)) φ.encode) + = List.replicate (((φ[j]'hj)[p]'hp).var) true := by + rw [litVarFn, halfFn_eq, List.length_drop, length_litSegFn_encode φ hj hp] + congr 1 + omega + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/CSPVerifier.lean b/Complexitylib/Classes/PCP/Internal/CSPVerifier.lean new file mode 100644 index 00000000..e4a843ab --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/CSPVerifier.lean @@ -0,0 +1,513 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.BinValLE +public import Complexitylib.Classes.P.Cobham.Internal.HeadOps +public import Complexitylib.Classes.P.Cobham.Internal.PVerdict +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P.DecisionFn +public import Complexitylib.Classes.PCP.Internal.PositionsFP +public import Complexitylib.Classes.PCP.Internal.SquareVerifier + +/-! +# A verifier for a constraint graph + +The verifier of a constraint graph picks an edge at random, reads the symbols +its two endpoints carry, and checks the constraint. The proof is the assignment, +written as one fixed-width block per vertex, so the queries are the two blocks. + +This module packages the algorithmic content a graph must supply — how many +edges, the endpoints of an edge, and the constraint — and turns it into a +`PCPVerifier`. Nothing here is about the graph's *quality*: completeness and +soundness are hypotheses on the supplied data, discharged elsewhere by Dinur's +amplification. + +## Main definitions + +- `Complexity.AlgCSP` — a constraint graph presented algorithmically + +## Main results + +- `Complexity.AlgCSP.cntU_mem_FP`, `Complexity.AlgCSP.posU_mem_FP` — the query + list is polynomial-time describable +- `Complexity.AlgCSP.verifier` — the verifier itself +- `Complexity.mem_PCP_of_algCSP` — a constraint graph with a gap puts its + language in `PCP` +-/ + +@[expose] public section + +namespace Complexity + +/-- A constraint graph presented the way an algorithm sees it: a count of edges, +the endpoints of each edge, and a decidable constraint. Indices are handled in +unary, which is what a polynomial-time loop can carry. -/ +structure AlgCSP where + /-- How many edges the graph on this input has. -/ + numEdges : List Bool → ℕ + /-- The edge count is polynomial-time computable in unary. -/ + numEdges_mem : (fun x : List Bool => List.replicate (numEdges x) true) ∈ FP + /-- The number of bits a symbol occupies. -/ + width : ℕ + /-- A symbol occupies at least one bit. -/ + width_pos : 0 < width + /-- The endpoints of an edge: `false` for the first, `true` for the second. -/ + vert : Bool → List Bool → ℕ → ℕ + /-- The endpoints are polynomial-time computable in unary. -/ + vert_mem : ∀ b, (fun w : List Bool => List.replicate + (vert b (pairFst w) (pairSnd w).length) true) ∈ FP + /-- The constraint, on `pair (pair x (unary e)) (the two symbol blocks)`. -/ + ok : Language + /-- The constraint is polynomial-time decidable. -/ + ok_mem : ok ∈ P + +namespace AlgCSP + +variable (A : AlgCSP) (p : Polynomial ℕ) + +/-- The edge a coin string names. -/ +def edgeIdx (z : List Bool) : ℕ := binValLE (pairSnd z) + +/-- That index in unary, as far as the clamp allows. -/ +noncomputable def edgeU (z : List Bool) : List Bool := unaryVal p z + +theorem edgeU_mem_FP : edgeU p ∈ FP := unaryVal_mem_FP p + +theorem edgeU_eq {z : List Bool} + (h : 2 ^ (pairSnd z).length ≤ p.eval z.length) : + edgeU p z = List.replicate (edgeIdx z) true := unaryVal_eq h + +/-- Is the named edge a real one? -/ +noncomputable def inRange (z : List Bool) : List Bool := + Cobham.lenLeFlag (List.replicate (A.numEdges (pairFst z)) true) + (true :: edgeU p z) + +theorem inRange_mem_FP : A.inRange p ∈ FP := by + have hn : (fun z : List Bool => + List.replicate (A.numEdges (pairFst z)) true) ∈ FP := by + have := mem_FP_comp Cobham.fstBlock_mem_FP A.numEdges_mem + simpa using this + exact lenLeFlagFn_mem_FP hn (mem_FP_comp (edgeU_mem_FP p) (Cobham.cons_mem_FP true)) + +theorem inRange_eq_true_iff {z : List Bool} + (h : 2 ^ (pairSnd z).length ≤ p.eval z.length) : + A.inRange p z = [true] ↔ edgeIdx z < A.numEdges (pairFst z) := by + rw [inRange, edgeU_eq p h, + Cobham.lenLeFlag_eq_true_iff, List.length_cons, List.length_replicate, + List.length_replicate] + omega + +/-- How many queries the verifier makes: both symbol blocks, or none when the +coin string names no edge. -/ +noncomputable def cntU (z : List Bool) : List Bool := + Cobham.selectHead (A.inRange p z) (List.replicate (2 * A.width) true) [] + +theorem cntU_mem_FP : A.cntU p ∈ FP := + Cobham.selectHeadFn_mem_FP (A.inRange_mem_FP p) + (constFn_mem_FP (List.replicate (2 * A.width) true)) (constFn_mem_FP []) + +/-! ### Where the verifier looks -/ + +/-- The endpoint a query index refers to: the first for the low half of the +queries, the second for the high half. -/ +noncomputable def vertU (b : Bool) (w : List Bool) : List Bool := + List.replicate (A.vert b (pairFst (pairFst w)) + (edgeU p (pairFst w)).length) true + +theorem vertU_mem_FP (b : Bool) : A.vertU p b ∈ FP := by + have hx : (fun w : List Bool => pairFst (pairFst w)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + have he : (fun w : List Bool => edgeU p (pairFst w)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP (edgeU_mem_FP p) + have := mem_FP_comp (Cobham.pairFn_mem_FP hx he) (A.vert_mem b) + refine mem_FP_of_eq this fun w => ?_ + rw [vertU, Function.comp_apply, pairFst_pair, pairSnd_pair] + +/-- Is this query in the low half? -/ +def lowFlag (w : List Bool) : List Bool := + Cobham.lenLeFlag (List.replicate A.width true) (true :: pairSnd w) + +theorem lowFlag_mem_FP : A.lowFlag ∈ FP := + lenLeFlagFn_mem_FP (constFn_mem_FP (List.replicate A.width true)) + (mem_FP_comp Cobham.sndBlock_mem_FP (Cobham.cons_mem_FP true)) + +theorem lowFlag_eq_true_iff (w : List Bool) : + A.lowFlag w = [true] ↔ (pairSnd w).length < A.width := by + rw [lowFlag, Cobham.lenLeFlag_eq_true_iff, List.length_cons, List.length_replicate] + omega + +/-- The offset inside the symbol block. -/ +def offU (w : List Bool) : List Bool := + Cobham.selectHead (A.lowFlag w) (pairSnd w) ((pairSnd w).drop A.width) + +theorem offU_mem_FP : A.offU ∈ FP := by + refine Cobham.selectHeadFn_mem_FP A.lowFlag_mem_FP Cobham.sndBlock_mem_FP ?_ + have := dropLenFn_mem_FP (constFn_mem_FP (List.replicate A.width true)) + Cobham.sndBlock_mem_FP + refine mem_FP_of_eq this fun w => ?_ + rw [List.length_replicate] + +/-- **The position a query reads**, in unary. -/ +noncomputable def posU (w : List Bool) : List Bool := + List.replicate + ((Cobham.selectHead (A.lowFlag w) (A.vertU p false w) (A.vertU p true w)).length + * A.width) true + ++ List.replicate (A.offU w).length true + +theorem posU_mem_FP : A.posU p ∈ FP := by + have hv : (fun w => Cobham.selectHead (A.lowFlag w) (A.vertU p false w) + (A.vertU p true w)) ∈ FP := + Cobham.selectHeadFn_mem_FP A.lowFlag_mem_FP (A.vertU_mem_FP p false) + (A.vertU_mem_FP p true) + have hmul : (fun w => List.replicate + ((Cobham.selectHead (A.lowFlag w) (A.vertU p false w) (A.vertU p true w)).length + * A.width) true) ∈ FP := by + have hb : (fun _ : List Bool => List.replicate A.width false) ∈ FP := + Cobham.const_replicate_mem_FP A.width + have hm := Cobham.mulLenFn_mem_FP hv hb + have := mem_FP_comp hm unaryLength_mem_FP + refine mem_FP_of_eq this fun w => ?_ + rw [Function.comp_apply, List.length_replicate, List.length_replicate] + have hoff : (fun w => List.replicate (A.offU w).length true) ∈ FP := by + have := mem_FP_comp A.offU_mem_FP unaryLength_mem_FP + simpa using this + exact Cobham.appendFn_mem_FP hmul hoff + +theorem cntU_eq_replicate (z : List Bool) : + A.cntU p z = List.replicate (A.cntU p z).length true := by + rw [cntU] + rcases Cobham.lenLeFlag_flag (List.replicate (A.numEdges (pairFst z)) true) + (true :: edgeU p z) with h | h <;> rw [inRange, h] + · rw [selectHead_cons_true, List.length_replicate] + · rw [selectHead_cons_false] + simp + +theorem posU_eq_replicate (w : List Bool) : + A.posU p w = List.replicate (A.posU p w).length true := by + rw [posU, List.length_append, List.length_replicate, List.length_replicate, + List.replicate_add] + +/-! ### The verifier -/ + +/-- How many queries, as a number. -/ +noncomputable def cnt (z : List Bool) : ℕ := (A.cntU p z).length + +/-- The `i`-th query position, as a number. -/ +noncomputable def pos (z : List Bool) (i : ℕ) : ℕ := + (A.posU p (pair z (List.replicate i true))).length + +/-- The argument the constraint is asked about. -/ +noncomputable def okArg (z : List Bool) : List Bool := + pair (pair (pairFst (pairFst z)) (edgeU p (pairFst z))) + (pairSnd z) + +theorem okArg_mem_FP : okArg p ∈ FP := by + have hx : (fun z : List Bool => pairFst (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + have he : (fun z : List Bool => edgeU p (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP (edgeU_mem_FP p) + exact Cobham.pairFn_mem_FP (Cobham.pairFn_mem_FP hx he) Cobham.sndBlock_mem_FP + +/-- The verdict: accept unless the coin string names a real edge whose +constraint fails. -/ +noncomputable def verdictLang : Language := + {z | A.inRange p (pairFst z) = [true] → okArg p z ∈ A.ok} + +theorem verdictLang_mem_P : A.verdictLang p ∈ P := by + obtain ⟨g, hgFP, hg⟩ := exists_decisionFn_of_mem_P A.ok_mem + have hin : (fun z : List Bool => A.inRange p (pairFst z)) ∈ FP := by + have := mem_FP_comp Cobham.fstBlock_mem_FP (A.inRange_mem_FP p) + simpa using this + have hok : (fun z : List Bool => [g (okArg p z)]) ∈ FP := by + have := mem_FP_comp (okArg_mem_FP p) hgFP + simpa using this + have hflag : (fun z : List Bool => + Cobham.selectHead (A.inRange p (pairFst z)) [g (okArg p z)] [true]) ∈ FP := + Cobham.selectHeadFn_mem_FP hin hok (constFn_mem_FP [true]) + refine mem_P_of_decisionFn hflag fun z => ?_ + show (A.inRange p (pairFst z) = [true] → okArg p z ∈ A.ok) ↔ _ + rcases Cobham.lenLeFlag_flag (List.replicate (A.numEdges + (pairFst (pairFst z))) true) + (true :: edgeU p (pairFst z)) with h | h + · have hv : A.inRange p (pairFst z) = [true] := by rw [inRange]; exact h + rw [hv, selectHead_cons_true] + simp only [List.mem_singleton, exists_eq_left, forall_const] + exact hg _ + · have hv : A.inRange p (pairFst z) = [false] := by rw [inRange]; exact h + rw [hv, selectHead_cons_false] + simp + +theorem cnt_le (z : List Bool) : A.cnt p z ≤ 2 * A.width := by + rw [cnt, cntU] + rcases Cobham.lenLeFlag_flag (List.replicate (A.numEdges (pairFst z)) true) + (true :: edgeU p z) with h | h <;> rw [inRange, h] + · rw [selectHead_cons_true, List.length_replicate] + · rw [selectHead_cons_false] + simp + +/-- **The verifier of a constraint graph.** -/ +noncomputable def verifier : PCPVerifier where + positions x ρ := (List.range (A.cnt p (pair x ρ))).map (A.pos p (pair x ρ)) + positions_mem := by + have hcnt : (fun z : List Bool => List.replicate (A.cnt p z) true) ∈ FP := by + refine mem_FP_of_eq (A.cntU_mem_FP p) fun z => ?_ + rw [cnt, ← cntU_eq_replicate] + obtain ⟨g, hg, hgspec⟩ := positions_mem_of_unary hcnt (A.posU_mem_FP p) + (fun z i => by + show A.posU p (pair z (List.replicate i true)) + = List.replicate (A.posU p (pair z (List.replicate i true))).length true + rw [← posU_eq_replicate]) + exact ⟨g, hg, fun x ρ => hgspec (pair x ρ)⟩ + verdict := A.verdictLang p + verdict_mem := A.verdictLang_mem_P p + +@[simp] theorem positions_verifier (x ρ : List Bool) : + (A.verifier p).positions x ρ + = (List.range (A.cnt p (pair x ρ))).map (A.pos p (pair x ρ)) := rfl + +theorem verifier_queryBounded : (A.verifier p).QueryBounded (fun _ => 2 * A.width) := by + intro x ρ + rw [positions_verifier, List.length_map, List.length_range] + exact A.cnt_le p _ + +theorem mem_verdict_verifier (z : List Bool) : + z ∈ (A.verifier p).verdict + ↔ (A.inRange p (pairFst z) = [true] → okArg p z ∈ A.ok) := Iff.rfl + +/-! ### What the verifier reads and decides -/ + +/-- The position query `i` reads, as a function of the edge alone. -/ +def posVal (x : List Bool) (e i : ℕ) : ℕ := + A.vert (decide (¬ i < A.width)) x e * A.width + (if i < A.width then i else i - A.width) + +theorem pos_eq {x ρ : List Bool} (h : 2 ^ ρ.length ≤ p.eval (pair x ρ).length) (i : ℕ) : + A.pos p (pair x ρ) i = A.posVal x (binValLE ρ) i := by + have hz : pairSnd (pair x ρ) = ρ := pairSnd_pair x ρ + have hE : edgeU p (pair x ρ) = List.replicate (binValLE ρ) true := by + have := edgeU_eq (p := p) (z := pair x ρ) (by rw [hz]; exact h) + rw [this, edgeIdx, hz] + set w := pair (pair x ρ) (List.replicate i true) with hw + have hsnd : pairSnd w = List.replicate i true := pairSnd_pair _ _ + have hfst : pairFst w = pair x ρ := pairFst_pair _ _ + have hlow : A.lowFlag w = if i < A.width then [true] else [false] := by + rcases Cobham.lenLeFlag_flag (List.replicate A.width true) + (true :: pairSnd w) with hf | hf + · rw [lowFlag, hf, if_pos] + rw [← lowFlag, A.lowFlag_eq_true_iff w, hsnd, List.length_replicate] at hf + exact hf + · rw [lowFlag, hf, if_neg] + intro hcon + have := (A.lowFlag_eq_true_iff w).mpr (by rw [hsnd, List.length_replicate]; exact hcon) + rw [lowFlag, hf] at this + simp at this + have hv : ∀ b, (A.vertU p b w).length = A.vert b x (binValLE ρ) := by + intro b + rw [vertU, List.length_replicate, hfst, hE, List.length_replicate, + pairFst_pair] + have hoff : (A.offU w).length = if i < A.width then i else i - A.width := by + rw [offU, hlow, hsnd] + by_cases hi : i < A.width + · rw [if_pos hi, selectHead_cons_true, List.length_replicate, if_pos hi] + · rw [if_neg hi, selectHead_cons_false, List.length_drop, List.length_replicate, + if_neg hi] + show (A.posU p w).length = _ + rw [posU, List.length_append, List.length_replicate, List.length_replicate, hoff, + posVal] + congr 2 + rw [hlow] + by_cases hi : i < A.width + · rw [if_pos hi, selectHead_cons_true, hv] + simp [hi] + · rw [if_neg hi, selectHead_cons_false, hv] + simp [hi] + +/-- Edge `e` is satisfied by the proof `π`. -/ +def Sat (x π : List Bool) (e : ℕ) : Prop := + pair (pair x (List.replicate e true)) + (PCPVerifier.answers π ((List.range (2 * A.width)).map (A.posVal x e))) ∈ A.ok + +theorem cnt_eq_of_inRange {z : List Bool} (h : A.inRange p z = [true]) : + A.cnt p z = 2 * A.width := by + rw [cnt, cntU, h, selectHead_cons_true, List.length_replicate] + +/-- **What the verifier decides.** It accepts unless the coin string names a +real edge that the proof fails to satisfy. -/ +theorem accepts_verifier_iff {x ρ : List Bool} + (h : 2 ^ ρ.length ≤ p.eval (pair x ρ).length) (π : List Bool) : + (A.verifier p).Accepts x π ρ + ↔ (binValLE ρ < A.numEdges x → A.Sat x π (binValLE ρ)) := by + have hz : pairSnd (pair x ρ) = ρ := pairSnd_pair x ρ + have hE : edgeU p (pair x ρ) = List.replicate (binValLE ρ) true := by + have := edgeU_eq (p := p) (z := pair x ρ) (by rw [hz]; exact h) + rw [this, edgeIdx, hz] + have hin : A.inRange p (pair x ρ) = [true] ↔ binValLE ρ < A.numEdges x := by + rw [A.inRange_eq_true_iff p (by rw [hz]; exact h), edgeIdx, hz, pairFst_pair] + set a := PCPVerifier.answers π ((A.verifier p).positions x ρ) with ha + have hacc : (A.verifier p).Accepts x π ρ + ↔ (A.inRange p (pair x ρ) = [true] → okArg p (pair (pair x ρ) a) ∈ A.ok) := by + rw [PCPVerifier.Accepts, mem_verdict_verifier, pairFst_pair] + have haeq : binValLE ρ < A.numEdges x → + a = PCPVerifier.answers π ((List.range (2 * A.width)).map (A.posVal x (binValLE ρ))) := by + intro hlt + rw [ha, positions_verifier, A.cnt_eq_of_inRange p (hin.mpr hlt)] + congr 1 + exact List.map_congr_left fun i _ => A.pos_eq p h i + rw [hacc, hin] + constructor + · intro hh hlt + have hok := hh hlt + rw [okArg, pairFst_pair, pairFst_pair, pairSnd_pair, hE] at hok + rw [Sat, ← haeq hlt] + exact hok + · intro hh hlt + have hs := hh hlt + rw [Sat] at hs + rw [okArg, pairFst_pair, pairFst_pair, pairSnd_pair, hE, + haeq hlt] + exact hs + +/-! ### How often the verifier accepts -/ + +open Classical in +theorem acceptEvent_eq {x : List Bool} {T : ℕ} + (h : 2 ^ T ≤ p.eval (2 * x.length + 2 + T)) (π : List Bool) : + (A.verifier p).acceptEvent T x π + = Finset.univ.filter (fun ρ : Fin T → Bool => + PCPVerifier.coinIndex ρ < A.numEdges x → A.Sat x π (PCPVerifier.coinIndex ρ)) := by + ext ρ + have hlen : (BitString.toList ρ).length = T := by simp + have hclamp : 2 ^ (BitString.toList ρ).length + ≤ p.eval (pair x (BitString.toList ρ)).length := by + rw [hlen, pair_length, hlen] + exact h + simp only [PCPVerifier.acceptEvent, Finset.mem_filter, Finset.mem_univ, true_and] + rw [A.accepts_verifier_iff p hclamp, binValLE_toList] + +open Classical in +theorem card_reject {x : List Bool} {T : ℕ} (hN : A.numEdges x ≤ 2 ^ T) (π : List Bool) : + ((Finset.univ.filter (fun ρ : Fin T → Bool => + PCPVerifier.coinIndex ρ < A.numEdges x → A.Sat x π (PCPVerifier.coinIndex ρ)))ᶜ).card + = ((Finset.range (A.numEdges x)).filter (fun e => ¬ A.Sat x π e)).card := by + classical + have hcompl : (Finset.univ.filter (fun ρ : Fin T → Bool => + PCPVerifier.coinIndex ρ < A.numEdges x → A.Sat x π (PCPVerifier.coinIndex ρ)))ᶜ + = Finset.univ.filter (fun ρ : Fin T → Bool => + ¬ (PCPVerifier.coinIndex ρ < A.numEdges x → A.Sat x π (PCPVerifier.coinIndex ρ))) := by + ext ρ + simp + rw [hcompl, card_filter_coinIndex T (fun e => ¬ (e < A.numEdges x → A.Sat x π e))] + congr 1 + ext e + simp only [Finset.mem_filter, Finset.mem_range, Classical.not_imp] + constructor + · rintro ⟨_, hlt, hns⟩ + exact ⟨hlt, hns⟩ + · rintro ⟨hlt, hns⟩ + exact ⟨lt_of_lt_of_le hlt hN, hlt, hns⟩ + +open Classical in +/-- **Perfect completeness.** A proof satisfying every edge is always +accepted. -/ +theorem eventProb_eq_one {x : List Bool} {T : ℕ} + (h : 2 ^ T ≤ p.eval (2 * x.length + 2 + T)) (hN : A.numEdges x ≤ 2 ^ T) + {π : List Bool} (hsat : ∀ e < A.numEdges x, A.Sat x π e) : + eventProb ((A.verifier p).acceptEvent T x π) = 1 := by + classical + have hrej : ((Finset.range (A.numEdges x)).filter (fun e => ¬ A.Sat x π e)).card = 0 := by + rw [Finset.card_eq_zero, Finset.filter_eq_empty_iff] + intro e he + exact not_not.mpr (hsat e (Finset.mem_range.mp he)) + have hc := A.card_reject hN π + rw [hrej] at hc + have hempty : (Finset.univ.filter (fun ρ : Fin T → Bool => + PCPVerifier.coinIndex ρ < A.numEdges x → A.Sat x π (PCPVerifier.coinIndex ρ)))ᶜ = ∅ := + Finset.card_eq_zero.mp hc + have : eventProb ((Finset.univ.filter (fun ρ : Fin T → Bool => + PCPVerifier.coinIndex ρ < A.numEdges x → A.Sat x π (PCPVerifier.coinIndex ρ)))ᶜ) = 0 := by + rw [hempty, eventProb_empty] + rw [A.acceptEvent_eq p h π] + rw [eventProb_compl] at this + linarith + +open Classical in +/-- **Soundness.** If no proof satisfies more than a `1 - gap` fraction of the +edges, the verifier accepts with probability at most `1 - gap / 2`. -/ +theorem eventProb_le {x : List Bool} {T : ℕ} {gap : ℚ} + (h : 2 ^ T ≤ p.eval (2 * x.length + 2 + T)) (hN : A.numEdges x ≤ 2 ^ T) + (hT : 2 ^ T ≤ 2 * A.numEdges x) {π : List Bool} + (hs : (((Finset.range (A.numEdges x)).filter (A.Sat x π)).card : ℚ) + ≤ (1 - gap) * A.numEdges x) : + eventProb ((A.verifier p).acceptEvent T x π) ≤ 1 - gap / 2 := by + classical + set N := A.numEdges x with hNdef + set S := ((Finset.range N).filter (A.Sat x π)).card with hS + set R := ((Finset.range N).filter (fun e => ¬ A.Sat x π e)).card with hR + have hsum : S + R = N := by + rw [hS, hR] + have := Finset.card_filter_add_card_filter_not + (s := Finset.range N) (p := A.Sat x π) + simpa using this + have hNpos : 0 < N := by + have h2 : (0 : ℕ) < 2 ^ T := Nat.two_pow_pos T + omega + have hTQ : (0 : ℚ) < (2 : ℚ) ^ T := by positivity + have hRQ : (R : ℚ) = (N : ℚ) - (S : ℚ) := by + have : (S : ℚ) + (R : ℚ) = (N : ℚ) := by exact_mod_cast hsum + linarith + have hRge : gap * (N : ℚ) ≤ (R : ℚ) := by + rw [hRQ] + nlinarith [hs] + have hTle : ((2 : ℚ) ^ T) ≤ 2 * (N : ℚ) := by + have : ((2 ^ T : ℕ) : ℚ) ≤ ((2 * N : ℕ) : ℚ) := by exact_mod_cast hT + push_cast at this + linarith + have hRnn : (0 : ℚ) ≤ (R : ℚ) := by positivity + have hdiv : (R : ℚ) / (2 * (N : ℚ)) ≤ (R : ℚ) / (2 : ℚ) ^ T := + div_le_div_of_nonneg_left hRnn hTQ hTle + have hgap : gap / 2 ≤ (R : ℚ) / (2 * (N : ℚ)) := by + rw [div_le_div_iff₀ (by norm_num) (by linarith)] + nlinarith [hRge] + rw [A.acceptEvent_eq p h π] + set F := Finset.univ.filter (fun ρ : Fin T → Bool => + PCPVerifier.coinIndex ρ < N → A.Sat x π (PCPVerifier.coinIndex ρ)) with hF + have hcompl : eventProb Fᶜ = 1 - eventProb F := eventProb_compl F + have hcard : Fᶜ.card = R := A.card_reject hN π + have hFc : eventProb Fᶜ = (R : ℚ) / 2 ^ T := by + rw [eventProb, hcard] + linarith [hgap, hdiv, hFc, hcompl] + +end AlgCSP + +open scoped Complexity in +open Classical in +/-- **A constraint graph with a gap puts its language in `PCP`.** Completeness +and soundness are hypotheses on the graph: a member has an assignment satisfying +every edge, and a non-member has none satisfying more than a `1 - gap` +fraction. -/ +theorem mem_PCP_of_algCSP (A : AlgCSP) (p : Polynomial ℕ) (t : ℕ → ℕ) + (ht : (fun x : List Bool => List.replicate (t x.length) true) ∈ FP) + (hclamp : ∀ n : ℕ, 2 ^ t n ≤ p.eval (2 * n + 2 + t n)) + (hN : ∀ x : List Bool, A.numEdges x ≤ 2 ^ t x.length) + (hT : ∀ x : List Bool, 2 ^ t x.length ≤ 2 * A.numEdges x) + {L : Language} {gap : ℚ} (hgap0 : 0 < gap) (hgap1 : gap ≤ 1) + (hcomp : ∀ x ∈ L, ∃ π : List Bool, ∀ e < A.numEdges x, A.Sat x π e) + (hsound : ∀ x ∉ L, ∀ π : List Bool, + (((Finset.range (A.numEdges x)).filter (A.Sat x π)).card : ℚ) + ≤ (1 - gap) * A.numEdges x) : + ∃ j : ℕ, L ∈ PCP (fun n => 2 ^ j * t n) (fun _ => 2 ^ j * (2 * A.width)) := by + classical + have hmem : L ∈ PCPWith t (fun _ => 2 * A.width) (1 - gap / 2) := by + refine ⟨A.verifier p, A.verifier_queryBounded p, ?_, ?_⟩ + · intro x hx + obtain ⟨π, hπ⟩ := hcomp x hx + exact ⟨π, A.eventProb_eq_one p (hclamp x.length) (hN x) hπ⟩ + · intro x hx π + exact A.eventProb_le p (hclamp x.length) (hN x) (hT x) (hsound x hx π) + refine mem_PCP_of_PCPWith ?_ ?_ ht hmem + · linarith + · linarith + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/Cheeger.lean b/Complexitylib/Classes/PCP/Internal/Cheeger.lean new file mode 100644 index 00000000..2b926fd7 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/Cheeger.lean @@ -0,0 +1,565 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.EdgeExpansion +public import Complexitylib.Classes.PCP.Internal.ExpanderPad + +/-! +# Cheeger's inequality: edge expansion gives a spectral gap + +`EdgeExpansion` proved that a spectral gap forces every set to have many +boundary darts. This module proves the converse — the harder direction of +Cheeger's inequality — in the form the rest of the development consumes: a +`SpectralBound` below one, for the graph with a self-loop added per dart. + +The argument is the classical one, made discrete. + +* The **Dirichlet form** `∑ (f u - f w)²` over darts equals + `2 d (‖f‖² - ⟨f, step f⟩)`. +* **Co-area.** For `ψ ≥ 0` supported on at most half the vertices, + `∑ |ψ u - ψ w| ≥ 2 h d ∑ ψ`: peel off the lowest positive level, apply the + expansion to the support, and induct on the support. +* **Cauchy–Schwarz** turns the co-area bound for `ψ = φ²` into + `h² d ∑ φ² ≤ Dirichlet φ` for nonnegative `φ` of small support. +* **A median split** extends this to every mean-zero `f`, losing nothing. +* **The lazy walk.** Adding `d` loops halves the step operator plus the + identity, which is positive semidefinite, and a Cauchy–Schwarz for + semidefinite forms turns the Rayleigh bound into an operator bound — no + spectral theorem needed. + +## Main definitions + +- `Complexity.RegGraph.EdgeExpansion` — every set of at most half the vertices + has at least `h d |S|` boundary darts +- `Complexity.RegGraph.dirichlet` — the Dirichlet form over darts + +## Main results + +- `Complexity.RegGraph.dirichlet_ge_of_edgeExpansion` — `h² d ‖f‖² ≤ Dirichlet f` + for mean-zero `f` +- `Complexity.RegGraph.spectralBound_padLoops_of_edgeExpansion` — the lazy + graph has bound `1 - h² / 4` +-/ + +@[expose] public section + +namespace Complexity + +namespace RegGraph + +variable (G : RegGraph) + +/-! ### Darts and their reversal -/ + +/-- The reversal of darts, as a permutation. -/ +def rotPerm : Equiv.Perm (G.V × G.D) := G.rot_involutive.toPerm + +theorem nbr_rot (p : G.V × G.D) : G.nbr (G.rot p).1 (G.rot p).2 = p.1 := by + show (G.rot (G.rot p)).1 = p.1 + rw [G.rot_involutive p] + +/-- **Reversing every dart** swaps the two ends in a sum. -/ +theorem sum_darts_swap (g : G.V → G.V → ℝ) : + ∑ p : G.V × G.D, g p.1 (G.nbr p.1 p.2) = ∑ p : G.V × G.D, g (G.nbr p.1 p.2) p.1 := by + refine Fintype.sum_equiv G.rotPerm _ _ fun p => ?_ + show g p.1 (G.nbr p.1 p.2) = g (G.nbr (G.rot p).1 (G.rot p).2) (G.rot p).1 + rw [nbr_rot] + rfl + +theorem sum_darts_fst (g : G.V → ℝ) : + ∑ p : G.V × G.D, g p.1 = (G.deg : ℝ) * ∑ v : G.V, g v := by + rw [Fintype.sum_prod_type] + simp only [Finset.sum_const, Finset.card_univ, card_eq_deg, nsmul_eq_mul] + rw [← Finset.mul_sum] + +theorem sum_darts_snd (g : G.V → ℝ) : + ∑ p : G.V × G.D, g (G.nbr p.1 p.2) = (G.deg : ℝ) * ∑ v : G.V, g v := by + rw [G.sum_darts_swap (fun _ w => g w)] + exact G.sum_darts_fst g + +theorem sum_mul_step (f g : G.V → ℝ) : + ∑ v : G.V, f v * G.step g v = (∑ p : G.V × G.D, f p.1 * g (G.nbr p.1 p.2)) / (G.deg : ℝ) := by + simp only [step, Fintype.sum_prod_type] + rw [Finset.sum_div] + refine Finset.sum_congr rfl fun v _ => ?_ + rw [← mul_div_assoc, Finset.mul_sum] + +/-! ### Edge expansion and the Dirichlet form -/ + +/-- **Edge expansion**: every set of at most half the vertices has at least +`h · d · |S|` darts leaving it. -/ +def EdgeExpansion (h : ℝ) : Prop := + ∀ S : Finset G.V, 2 * S.card ≤ G.order → + h * (G.deg : ℝ) * (S.card : ℝ) ≤ ((G.dartsBetween S Sᶜ).card : ℝ) + +/-- The Dirichlet form: the sum over darts of the squared difference. -/ +noncomputable def dirichlet (f : G.V → ℝ) : ℝ := + ∑ p : G.V × G.D, (f p.1 - f (G.nbr p.1 p.2)) ^ 2 + +theorem dirichlet_nonneg (f : G.V → ℝ) : 0 ≤ G.dirichlet f := + Finset.sum_nonneg fun _ _ => sq_nonneg _ + +/-- The Dirichlet form is invariant under adding a constant. -/ +theorem dirichlet_sub_const (f : G.V → ℝ) (c : ℝ) : + G.dirichlet (fun v => f v - c) = G.dirichlet f := by + simp only [dirichlet] + refine Finset.sum_congr rfl fun p _ => ?_ + ring + +/-! ### Co-area -/ + +/-- The darts crossing out of or into `S`, counted with the boundary in both +directions. -/ +theorem sum_darts_boundary (S : Finset G.V) : + ∑ p : G.V × G.D, (if p.1 ∈ S ∧ G.nbr p.1 p.2 ∉ S then (1 : ℝ) else 0) + + ∑ p : G.V × G.D, (if p.1 ∉ S ∧ G.nbr p.1 p.2 ∈ S then (1 : ℝ) else 0) + = 2 * ((G.dartsBetween S Sᶜ).card : ℝ) := by + classical + have h1 : ∑ p : G.V × G.D, (if p.1 ∈ S ∧ G.nbr p.1 p.2 ∉ S then (1 : ℝ) else 0) + = ((G.dartsBetween S Sᶜ).card : ℝ) := by + rw [dartsBetween, Finset.card_filter] + push_cast + refine Finset.sum_congr rfl fun p _ => ?_ + simp + have h2 : ∑ p : G.V × G.D, (if p.1 ∉ S ∧ G.nbr p.1 p.2 ∈ S then (1 : ℝ) else 0) + = ((G.dartsBetween S Sᶜ).card : ℝ) := by + rw [← h1, G.sum_darts_swap (fun u w => if u ∉ S ∧ w ∈ S then (1 : ℝ) else 0)] + refine Finset.sum_congr rfl fun p _ => ?_ + simp only [and_comm] + rw [h1, h2] + ring + +/-- The support of a function. -/ +noncomputable def support (ψ : G.V → ℝ) : Finset G.V := Finset.univ.filter fun v => ψ v ≠ 0 + +theorem mem_support_iff (ψ : G.V → ℝ) (v : G.V) : v ∈ G.support ψ ↔ ψ v ≠ 0 := by + simp [support] + +/-- **Co-area.** For `ψ ≥ 0` supported on at most half the vertices, +`∑ |ψ u - ψ w| ≥ 2 h d ∑ ψ`. -/ +theorem coarea {h : ℝ} (hexp : G.EdgeExpansion h) : + ∀ (n : ℕ) (ψ : G.V → ℝ), (G.support ψ).card = n → (∀ v, 0 ≤ ψ v) + → 2 * (G.support ψ).card ≤ G.order + → 2 * h * (G.deg : ℝ) * ∑ v : G.V, ψ v + ≤ ∑ p : G.V × G.D, |ψ p.1 - ψ (G.nbr p.1 p.2)| := by + classical + intro n + induction n using Nat.strong_induction_on with + | _ n ih => + intro ψ hn hpos hhalf + by_cases hemp : G.support ψ = ∅ + · -- everything vanishes + have hzero : ∀ v, ψ v = 0 := fun v => by + by_contra hv + have : v ∈ G.support ψ := by simp [support, hv] + rw [hemp] at this + exact absurd this (Finset.notMem_empty v) + simp [hzero] + · -- peel the lowest positive level + have hne : (G.support ψ).Nonempty := Finset.nonempty_iff_ne_empty.2 hemp + set S := G.support ψ with hS + have hmem : ∀ v, v ∈ S ↔ ψ v ≠ 0 := fun v => by rw [hS]; exact G.mem_support_iff ψ v + have hz : ∀ v, v ∉ S → ψ v = 0 := fun v hv => by + by_contra h + exact hv ((hmem v).2 h) + set μ := (S.image ψ).min' (hne.image ψ) with hμ + have hμmem : μ ∈ S.image ψ := Finset.min'_mem _ _ + obtain ⟨v₀, hv₀S, hv₀⟩ := Finset.mem_image.1 hμmem + have hμpos : 0 < μ := by + rw [← hv₀] + exact lt_of_le_of_ne (hpos v₀) (Ne.symm ((hmem v₀).1 hv₀S)) + have hμle : ∀ v ∈ S, μ ≤ ψ v := fun v hv => + Finset.min'_le _ _ (Finset.mem_image_of_mem ψ hv) + -- the peeled function + set ψ' : G.V → ℝ := fun v => if v ∈ S then ψ v - μ else 0 with hψ' + have hψ'in : ∀ v, v ∈ S → ψ' v = ψ v - μ := fun v hv => by simp [hψ', hv] + have hψ'out : ∀ v, v ∉ S → ψ' v = 0 := fun v hv => by simp [hψ', hv] + have hψ'pos : ∀ v, 0 ≤ ψ' v := fun v => by + by_cases hv : v ∈ S + · rw [hψ'in v hv]; linarith [hμle v hv] + · rw [hψ'out v hv] + have hψ'supp : G.support ψ' ⊆ S.erase v₀ := by + intro v hv + rw [G.mem_support_iff] at hv + rw [Finset.mem_erase] + by_cases hvS : v ∈ S + · refine ⟨fun hvv => ?_, hvS⟩ + rw [hψ'in v hvS, hvv, hv₀, sub_self] at hv + exact hv rfl + · rw [hψ'out v hvS] at hv + exact absurd rfl hv + have hcard' : (G.support ψ').card < n := by + rw [← hn] + calc (G.support ψ').card ≤ (S.erase v₀).card := Finset.card_le_card hψ'supp + _ < S.card := Finset.card_erase_lt_of_mem hv₀S + have hhalf' : 2 * (G.support ψ').card ≤ G.order := by + have := Finset.card_le_card hψ'supp + omega + have ihψ' := ih _ hcard' ψ' rfl hψ'pos hhalf' + -- relate the two functions + have hsum : ∑ v : G.V, ψ v = ∑ v : G.V, ψ' v + μ * S.card := by + have : ∀ v, ψ v = ψ' v + (if v ∈ S then μ else 0) := fun v => by + by_cases hv : v ∈ S + · rw [hψ'in v hv, if_pos hv]; ring + · rw [hψ'out v hv, if_neg hv, hz v hv]; ring + rw [Finset.sum_congr rfl fun v _ => this v, Finset.sum_add_distrib] + congr 1 + rw [Finset.sum_ite_mem, Finset.univ_inter, Finset.sum_const, nsmul_eq_mul, mul_comm] + have hdart : ∀ p : G.V × G.D, |ψ p.1 - ψ (G.nbr p.1 p.2)| + = |ψ' p.1 - ψ' (G.nbr p.1 p.2)| + + μ * ((if p.1 ∈ S ∧ G.nbr p.1 p.2 ∉ S then (1 : ℝ) else 0) + + (if p.1 ∉ S ∧ G.nbr p.1 p.2 ∈ S then (1 : ℝ) else 0)) := by + intro p + by_cases hu : p.1 ∈ S <;> by_cases hw : G.nbr p.1 p.2 ∈ S + · rw [hψ'in _ hu, hψ'in _ hw, if_neg (by tauto), if_neg (by tauto)] + have : ψ p.1 - μ - (ψ (G.nbr p.1 p.2) - μ) = ψ p.1 - ψ (G.nbr p.1 p.2) := by ring + rw [this] + ring + · rw [hψ'in _ hu, hψ'out _ hw, hz _ hw, if_pos ⟨hu, hw⟩, if_neg (by tauto), sub_zero, + sub_zero, abs_of_nonneg (hpos _), abs_of_nonneg (by linarith [hμle _ hu])] + ring + · rw [hψ'out _ hu, hψ'in _ hw, hz _ hu, if_neg (by tauto), if_pos ⟨hu, hw⟩, zero_sub, + zero_sub, abs_neg, abs_neg, abs_of_nonneg (hpos _), + abs_of_nonneg (by linarith [hμle _ hw])] + ring + · rw [hψ'out _ hu, hψ'out _ hw, hz _ hu, hz _ hw, if_neg (by tauto), if_neg (by tauto)] + simp + rw [Finset.sum_congr rfl fun p _ => hdart p, Finset.sum_add_distrib, ← Finset.mul_sum, + Finset.sum_add_distrib, G.sum_darts_boundary S, hsum] + have hexpS := hexp S hhalf + have hd : (0 : ℝ) ≤ G.deg := by positivity + nlinarith [ihψ', hexpS, hμpos, hd] + +/-! ### The core bound for small support -/ + +theorem support_sq (φ : G.V → ℝ) : G.support (fun v => (φ v) ^ 2) = G.support φ := by + ext v + simp [support, pow_eq_zero_iff] + +/-- **Small support.** For `φ ≥ 0` supported on at most half the vertices, +`h² d ‖φ‖² ≤ Dirichlet φ`. -/ +theorem dirichlet_ge_of_support {h : ℝ} (hexp : G.EdgeExpansion h) (hh : 0 ≤ h) + (φ : G.V → ℝ) (hpos : ∀ v, 0 ≤ φ v) (hhalf : 2 * (G.support φ).card ≤ G.order) : + h ^ 2 * (G.deg : ℝ) * ∑ v : G.V, (φ v) ^ 2 ≤ G.dirichlet φ := by + classical + have hd : (0 : ℝ) < G.deg := by exact_mod_cast G.deg_pos + have hco := G.coarea hexp _ (fun v => (φ v) ^ 2) rfl (fun v => sq_nonneg _) + (by rw [support_sq]; exact hhalf) + -- `|a² - b²| = |a - b| (a + b)` for nonnegative `a, b` + have habs : ∀ p : G.V × G.D, |(φ p.1) ^ 2 - (φ (G.nbr p.1 p.2)) ^ 2| + = |φ p.1 - φ (G.nbr p.1 p.2)| * (φ p.1 + φ (G.nbr p.1 p.2)) := by + intro p + rw [sq_sub_sq, abs_mul, abs_of_nonneg (by linarith [hpos p.1, hpos (G.nbr p.1 p.2)])] + ring + rw [Finset.sum_congr rfl fun p _ => habs p] at hco + -- Cauchy–Schwarz + have hcs := Finset.sum_mul_sq_le_sq_mul_sq (Finset.univ : Finset (G.V × G.D)) + (fun p => |φ p.1 - φ (G.nbr p.1 p.2)|) (fun p => φ p.1 + φ (G.nbr p.1 p.2)) + simp only [sq_abs] at hcs + have hsumsq : ∑ p : G.V × G.D, (φ p.1 + φ (G.nbr p.1 p.2)) ^ 2 + ≤ 4 * (G.deg : ℝ) * ∑ v : G.V, (φ v) ^ 2 := by + have : ∀ p : G.V × G.D, (φ p.1 + φ (G.nbr p.1 p.2)) ^ 2 + ≤ 2 * ((φ p.1) ^ 2 + (φ (G.nbr p.1 p.2)) ^ 2) := fun p => by + nlinarith [sq_nonneg (φ p.1 - φ (G.nbr p.1 p.2))] + calc ∑ p : G.V × G.D, (φ p.1 + φ (G.nbr p.1 p.2)) ^ 2 + ≤ ∑ p : G.V × G.D, 2 * ((φ p.1) ^ 2 + (φ (G.nbr p.1 p.2)) ^ 2) := + Finset.sum_le_sum fun p _ => this p + _ = 4 * (G.deg : ℝ) * ∑ v : G.V, (φ v) ^ 2 := by + rw [← Finset.mul_sum, Finset.sum_add_distrib, G.sum_darts_fst (fun v => (φ v) ^ 2), + G.sum_darts_snd (fun v => (φ v) ^ 2)] + ring + have hS : 0 ≤ ∑ v : G.V, (φ v) ^ 2 := Finset.sum_nonneg fun v _ => sq_nonneg _ + have hD : 0 ≤ G.dirichlet φ := G.dirichlet_nonneg φ + -- `dirichlet φ` is the first factor of Cauchy–Schwarz + have hDeq : ∑ p : G.V × G.D, (φ p.1 - φ (G.nbr p.1 p.2)) ^ 2 = G.dirichlet φ := rfl + rw [hDeq] at hcs + set A := 2 * h * (G.deg : ℝ) * ∑ v : G.V, (φ v) ^ 2 with hA + set B := ∑ p : G.V × G.D, |φ p.1 - φ (G.nbr p.1 p.2)| * (φ p.1 + φ (G.nbr p.1 p.2)) with hB + have hA0 : 0 ≤ A := by rw [hA]; positivity + have hAB : A ^ 2 ≤ B ^ 2 := by + have hB0 : 0 ≤ B := le_trans hA0 hco + nlinarith [hco, hA0, hB0] + have hkey : A ^ 2 ≤ G.dirichlet φ * (4 * (G.deg : ℝ) * ∑ v : G.V, (φ v) ^ 2) := + le_trans hAB (le_trans hcs (mul_le_mul_of_nonneg_left hsumsq hD)) + rw [hA] at hkey + rcases hS.eq_or_lt with hS0 | hS0 + · rw [← hS0, mul_zero]; exact hD + · have h4 : 0 < 4 * (G.deg : ℝ) * ∑ v : G.V, (φ v) ^ 2 := by positivity + have : (2 * h * (G.deg : ℝ) * ∑ v : G.V, (φ v) ^ 2) ^ 2 + = (h ^ 2 * (G.deg : ℝ) * ∑ v : G.V, (φ v) ^ 2) + * (4 * (G.deg : ℝ) * ∑ v : G.V, (φ v) ^ 2) := by + ring + rw [this] at hkey + exact le_of_mul_le_mul_right hkey h4 + +/-! ### The median split -/ + +/-- **A median.** Some value has at most half the vertices strictly above it and at +most half strictly below. -/ +theorem exists_median (f : G.V → ℝ) : + ∃ c : ℝ, 2 * (Finset.univ.filter fun v => c < f v).card ≤ G.order + ∧ 2 * (Finset.univ.filter fun v => f v < c).card ≤ G.order := by + classical + rcases isEmpty_or_nonempty G.V with hV | hV + · exact ⟨0, by simp, by simp⟩ + haveI : Nonempty G.V := hV + set T : Finset ℝ := (Finset.univ.image f).filter + fun t => 2 * (Finset.univ.filter fun v => t < f v).card ≤ G.order with hT + have hTne : T.Nonempty := by + refine ⟨(Finset.univ.image f).max' (Finset.univ_nonempty.image f), ?_⟩ + rw [hT, Finset.mem_filter] + refine ⟨Finset.max'_mem _ _, ?_⟩ + have : (Finset.univ.filter fun v => + (Finset.univ.image f).max' (Finset.univ_nonempty.image f) < f v) = ∅ := by + rw [Finset.filter_eq_empty_iff] + intro v _ hlt + exact absurd (Finset.le_max' _ _ (Finset.mem_image_of_mem f (Finset.mem_univ v))) + (not_le.2 hlt) + rw [this] + simp + set c := T.min' hTne with hc + have hcT : c ∈ T := Finset.min'_mem _ _ + rw [hT, Finset.mem_filter] at hcT + refine ⟨c, hcT.2, ?_⟩ + by_contra hcon + push Not at hcon + -- the largest value below `c` would be a smaller member of `T` + set L := Finset.univ.filter fun v => f v < c with hL + have hLne : L.Nonempty := by + rw [← Finset.card_pos] + omega + set t' := (L.image f).max' (hLne.image f) with ht' + have ht'mem : t' ∈ L.image f := Finset.max'_mem _ _ + obtain ⟨v', hv'L, hv'⟩ := Finset.mem_image.1 ht'mem + have hv'c : f v' < c := by + have := hv'L + rw [hL, Finset.mem_filter] at this + exact this.2 + have ht'c : t' < c := by rw [← hv']; exact hv'c + -- `{f > t'} = {f ≥ c}` + have hset : (Finset.univ.filter fun v => t' < f v) = Finset.univ.filter fun v => ¬ f v < c := by + ext v + simp only [Finset.mem_filter, Finset.mem_univ, true_and, not_lt] + constructor + · intro hlt + by_contra hvc + push Not at hvc + have hvL : v ∈ L := by rw [hL, Finset.mem_filter]; exact ⟨Finset.mem_univ _, hvc⟩ + have := Finset.le_max' (L.image f) (f v) (Finset.mem_image_of_mem f hvL) + rw [← ht'] at this + linarith + · intro hge + linarith + have hcard : (Finset.univ.filter fun v => ¬ f v < c).card + L.card = G.order := by + have := Finset.card_filter_add_card_filter_not (s := (Finset.univ : Finset G.V)) + (fun v => f v < c) + rw [Finset.card_univ] at this + rw [hL, add_comm] + exact this + have ht'T : t' ∈ T := by + rw [hT, Finset.mem_filter] + refine ⟨?_, ?_⟩ + · rw [← hv'] + exact Finset.mem_image_of_mem f (Finset.mem_univ v') + · rw [hset] + omega + have := Finset.min'_le T t' ht'T + rw [← hc] at this + linarith + +theorem support_max_subset (g : G.V → ℝ) : + G.support (fun v => max (g v) 0) ⊆ Finset.univ.filter fun v => 0 < g v := by + intro v hv + rw [mem_support_iff] at hv + simp only [Finset.mem_filter, Finset.mem_univ, true_and] + by_contra hle + push Not at hle + exact hv (max_eq_right hle) + +/-- **Cheeger's inequality, Dirichlet form.** For mean-zero `f`, +`h² d ‖f‖² ≤ Dirichlet f`. -/ +theorem dirichlet_ge_of_edgeExpansion {h : ℝ} (hexp : G.EdgeExpansion h) (hh : 0 ≤ h) + (f : G.V → ℝ) (hf : ∑ v : G.V, f v = 0) : + h ^ 2 * (G.deg : ℝ) * ∑ v : G.V, (f v) ^ 2 ≤ G.dirichlet f := by + classical + obtain ⟨c, hc₁, hc₂⟩ := G.exists_median f + set g : G.V → ℝ := fun v => f v - c with hg + set gp : G.V → ℝ := fun v => max (g v) 0 with hgp + set gm : G.V → ℝ := fun v => max (-g v) 0 with hgm + -- supports + have hsp : 2 * (G.support gp).card ≤ G.order := by + have : (G.support gp).card ≤ (Finset.univ.filter fun v => 0 < g v).card := + Finset.card_le_card (G.support_max_subset g) + have heq : (Finset.univ.filter fun v => 0 < g v) = Finset.univ.filter fun v => c < f v := by + ext v; simp [hg] + rw [heq] at this + omega + have hsm : 2 * (G.support gm).card ≤ G.order := by + have : (G.support gm).card ≤ (Finset.univ.filter fun v => 0 < -g v).card := + Finset.card_le_card (G.support_max_subset (fun v => -g v)) + have heq : (Finset.univ.filter fun v => 0 < -g v) = Finset.univ.filter fun v => f v < c := by + ext v; simp [hg] + rw [heq] at this + omega + have hp := G.dirichlet_ge_of_support hexp hh gp (fun v => le_max_right _ _) hsp + have hm := G.dirichlet_ge_of_support hexp hh gm (fun v => le_max_right _ _) hsm + -- squares split + have hsq : ∀ v, (g v) ^ 2 = (gp v) ^ 2 + (gm v) ^ 2 := by + intro v + simp only [hgp, hgm] + rcases le_total (g v) 0 with hle | hle + · rw [max_eq_right hle, max_eq_left (by linarith)]; ring + · rw [max_eq_left hle, max_eq_right (by linarith)]; ring + -- Dirichlet forms split, with slack + have hdir : G.dirichlet gp + G.dirichlet gm ≤ G.dirichlet g := by + simp only [dirichlet] + rw [← Finset.sum_add_distrib] + refine Finset.sum_le_sum fun p _ => ?_ + simp only [hgp, hgm] + rcases le_total (g p.1) 0 with h1 | h1 <;> rcases le_total (g (G.nbr p.1 p.2)) 0 with h2 | h2 + · rw [max_eq_right h1, max_eq_right h2, max_eq_left (by linarith), max_eq_left (by linarith)] + nlinarith + · rw [max_eq_right h1, max_eq_left h2, max_eq_left (by linarith), max_eq_right (by linarith)] + nlinarith [mul_nonpos_iff.2 (Or.inr ⟨h1, h2⟩)] + · rw [max_eq_left h1, max_eq_right h2, max_eq_right (by linarith), max_eq_left (by linarith)] + nlinarith [mul_nonpos_iff.2 (Or.inl ⟨h1, h2⟩)] + · rw [max_eq_left h1, max_eq_left h2, max_eq_right (by linarith), max_eq_right (by linarith)] + nlinarith + -- `‖g‖² ≥ ‖f‖²` + have hnorm : ∑ v : G.V, (f v) ^ 2 ≤ ∑ v : G.V, (g v) ^ 2 := by + have : ∀ v, (g v) ^ 2 = (f v) ^ 2 - 2 * c * f v + c ^ 2 := fun v => by + simp only [hg]; ring + rw [Finset.sum_congr rfl fun v _ => this v, Finset.sum_add_distrib, Finset.sum_sub_distrib, + ← Finset.mul_sum, hf] + have : 0 ≤ ∑ _v : G.V, c ^ 2 := Finset.sum_nonneg fun _ _ => sq_nonneg _ + linarith + have hdirg : G.dirichlet g = G.dirichlet f := G.dirichlet_sub_const f c + have hsplit : ∑ v : G.V, (g v) ^ 2 = ∑ v : G.V, (gp v) ^ 2 + ∑ v : G.V, (gm v) ^ 2 := by + rw [← Finset.sum_add_distrib] + exact Finset.sum_congr rfl fun v _ => hsq v + have hd : (0 : ℝ) ≤ h ^ 2 * G.deg := by positivity + calc h ^ 2 * (G.deg : ℝ) * ∑ v : G.V, (f v) ^ 2 + ≤ h ^ 2 * (G.deg : ℝ) * ∑ v : G.V, (g v) ^ 2 := mul_le_mul_of_nonneg_left hnorm hd + _ = h ^ 2 * (G.deg : ℝ) * ∑ v : G.V, (gp v) ^ 2 + + h ^ 2 * (G.deg : ℝ) * ∑ v : G.V, (gm v) ^ 2 := by rw [hsplit]; ring + _ ≤ G.dirichlet gp + G.dirichlet gm := add_le_add hp hm + _ ≤ G.dirichlet g := hdir + _ = G.dirichlet f := hdirg + +/-! ### The lazy walk -/ + +/-- The lazy quadratic form, as an inner product of dart sums. -/ +noncomputable def lazyQ (f g : G.V → ℝ) : ℝ := + ∑ p : G.V × G.D, (f p.1 + f (G.nbr p.1 p.2)) * (g p.1 + g (G.nbr p.1 p.2)) + +theorem step_lazy (f : G.V → ℝ) (v : G.V) : + (G.padLoops G.deg).step f v = (G.step f v + f v) / 2 := by + rw [step_padLoops] + have hd : (G.deg : ℝ) ≠ 0 := G.deg_ne_zero + field_simp + ring + +/-- **The lazy step against a function is the lazy form.** -/ +theorem sum_lazy_mul (f g : G.V → ℝ) : + ∑ v : G.V, (G.padLoops G.deg).step f v * g v = G.lazyQ f g / (4 * (G.deg : ℝ)) := by + have hd : (G.deg : ℝ) ≠ 0 := G.deg_ne_zero + simp only [step_lazy, lazyQ] + have hexp : ∀ p : G.V × G.D, (f p.1 + f (G.nbr p.1 p.2)) * (g p.1 + g (G.nbr p.1 p.2)) + = f p.1 * g p.1 + f (G.nbr p.1 p.2) * g (G.nbr p.1 p.2) + + (f p.1 * g (G.nbr p.1 p.2) + f (G.nbr p.1 p.2) * g p.1) := fun p => by ring + rw [Finset.sum_congr rfl fun p _ => hexp p, Finset.sum_add_distrib, Finset.sum_add_distrib, + Finset.sum_add_distrib, G.sum_darts_fst (fun v => f v * g v), + G.sum_darts_snd (fun v => f v * g v), G.sum_darts_swap (fun u w => f w * g u)] + have h1 : ∑ v : G.V, (G.step f v + f v) / 2 * g v + = (∑ v : G.V, g v * G.step f v + ∑ v : G.V, f v * g v) / 2 := by + rw [← Finset.sum_add_distrib, Finset.sum_div] + exact Finset.sum_congr rfl fun v _ => by ring + have hswap : ∑ p : G.V × G.D, g p.1 * f (G.nbr p.1 p.2) + = ∑ p : G.V × G.D, f p.1 * g (G.nbr p.1 p.2) := by + rw [G.sum_darts_swap (fun u w => g u * f w)] + exact Finset.sum_congr rfl fun p _ => mul_comm _ _ + rw [h1, sum_mul_step, hswap] + field_simp + ring + +theorem lazyQ_self (f : G.V → ℝ) : + G.lazyQ f f = 4 * (G.deg : ℝ) * ∑ v : G.V, (f v) ^ 2 - G.dirichlet f := by + simp only [lazyQ, dirichlet] + have : ∀ p : G.V × G.D, (f p.1 + f (G.nbr p.1 p.2)) * (f p.1 + f (G.nbr p.1 p.2)) + = 2 * ((f p.1) ^ 2 + (f (G.nbr p.1 p.2)) ^ 2) - (f p.1 - f (G.nbr p.1 p.2)) ^ 2 := + fun p => by ring + rw [Finset.sum_congr rfl fun p _ => this p, Finset.sum_sub_distrib, ← Finset.mul_sum, + Finset.sum_add_distrib, G.sum_darts_fst (fun v => (f v) ^ 2), + G.sum_darts_snd (fun v => (f v) ^ 2)] + ring + +theorem lazyQ_nonneg (f : G.V → ℝ) : 0 ≤ G.lazyQ f f := + Finset.sum_nonneg fun _ _ => mul_self_nonneg _ + +theorem lazyQ_sq_le (f g : G.V → ℝ) : (G.lazyQ f g) ^ 2 ≤ G.lazyQ f f * G.lazyQ g g := by + have := Finset.sum_mul_sq_le_sq_mul_sq (Finset.univ : Finset (G.V × G.D)) + (fun p => f p.1 + f (G.nbr p.1 p.2)) (fun p => g p.1 + g (G.nbr p.1 p.2)) + simp only [lazyQ, sq] at this ⊢ + exact this + +theorem sum_lazy_step (f : G.V → ℝ) : + ∑ v : G.V, (G.padLoops G.deg).step f v = ∑ v : G.V, f v := by + simp only [step_lazy] + rw [← Finset.sum_div, Finset.sum_add_distrib, sum_step] + ring + +/-- **Cheeger's inequality, spectral form.** Edge expansion `h` gives the lazy +graph — `d` self-loops added — the spectral bound `1 - h² / 4`. -/ +theorem spectralBound_padLoops_of_edgeExpansion {h : ℝ} (hexp : G.EdgeExpansion h) (hh : 0 ≤ h) : + (G.padLoops G.deg).SpectralBound (1 - h ^ 2 / 4) := by + intro f hf + have hd : (0 : ℝ) < G.deg := by exact_mod_cast G.deg_pos + set B : G.V → ℝ := (G.padLoops G.deg).step f with hB + have hBmean : ∑ v : G.V, B v = 0 := by rw [hB, sum_lazy_step]; exact hf + -- the Rayleigh bound, on `f` and on `B f` + have hray : ∀ g : G.V → ℝ, ∑ v : G.V, g v = 0 → + G.lazyQ g g ≤ (4 * (G.deg : ℝ) * (1 - h ^ 2 / 4)) * ∑ v : G.V, (g v) ^ 2 := by + intro g hg + rw [lazyQ_self] + have := G.dirichlet_ge_of_edgeExpansion hexp hh g hg + linarith + have hf' := hray f hf + have hB' := hray B hBmean + -- `‖B f‖²` through the lazy form + have hBB : ∑ v : G.V, (B v) ^ 2 = G.lazyQ f B / (4 * (G.deg : ℝ)) := by + rw [← sum_lazy_mul, hB] + exact Finset.sum_congr rfl fun v _ => by ring + have hcs := G.lazyQ_sq_le f B + have hQf := G.lazyQ_nonneg f + have hQB := G.lazyQ_nonneg B + have hSB : 0 ≤ ∑ v : G.V, (B v) ^ 2 := Finset.sum_nonneg fun v _ => sq_nonneg _ + show ∑ v : G.V, (B v) ^ 2 ≤ (1 - h ^ 2 / 4) ^ 2 * ∑ v : G.V, (f v) ^ 2 + set lam := 1 - h ^ 2 / 4 with hlam + have hprod : G.lazyQ f f * G.lazyQ B B + ≤ (4 * (G.deg : ℝ) * lam * ∑ v : G.V, (f v) ^ 2) + * (4 * (G.deg : ℝ) * lam * ∑ v : G.V, (B v) ^ 2) := + mul_le_mul hf' hB' hQB (le_trans hQf hf') + have hQfB : G.lazyQ f B = 4 * (G.deg : ℝ) * ∑ v : G.V, (B v) ^ 2 := by + rw [hBB]; field_simp + rw [hQfB] at hcs + have hkey : (4 * (G.deg : ℝ)) ^ 2 * (∑ v : G.V, (B v) ^ 2) ^ 2 + ≤ (4 * (G.deg : ℝ)) ^ 2 * (lam ^ 2 * ∑ v : G.V, (f v) ^ 2) * ∑ v : G.V, (B v) ^ 2 := by + nlinarith [hcs, hprod] + rcases hSB.eq_or_lt with h0 | h0 + · rw [← h0]; positivity + · have h16 : 0 < (4 * (G.deg : ℝ)) ^ 2 * ∑ v : G.V, (B v) ^ 2 := by positivity + have : (4 * (G.deg : ℝ)) ^ 2 * (∑ v : G.V, (B v) ^ 2) ^ 2 + = ((4 * (G.deg : ℝ)) ^ 2 * ∑ v : G.V, (B v) ^ 2) * ∑ v : G.V, (B v) ^ 2 := by ring + rw [this] at hkey + have : (4 * (G.deg : ℝ)) ^ 2 * (lam ^ 2 * ∑ v : G.V, (f v) ^ 2) * ∑ v : G.V, (B v) ^ 2 + = ((4 * (G.deg : ℝ)) ^ 2 * ∑ v : G.V, (B v) ^ 2) * (lam ^ 2 * ∑ v : G.V, (f v) ^ 2) := by + ring + rw [this] at hkey + exact le_of_mul_le_mul_left hkey h16 + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/CloudCount.lean b/Complexitylib/Classes/PCP/Internal/CloudCount.lean new file mode 100644 index 00000000..34bceba1 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/CloudCount.lean @@ -0,0 +1,109 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.DegreeReduction +public import Complexitylib.Classes.PCP.Internal.RankCount + +/-! +# A cloud, by counting + +Degree reduction rotates inside a cloud through the cloud's enumeration: the +half-edge it starts from is located by `List.idxOf`, and the one it lands on is +read off by position. Neither operation is available to an algorithm, which can +only count. This module replaces both by counts of half-edge numbers. + +The cloud is enumerated in order of those numbers, so a half-edge's position is +the number of smaller numbers in the cloud, and the half-edge at a position is +the one whose number has that many smaller numbers below it. + +## Main definitions + +- `Complexity.ConstraintGraph.cloudCodes` — the numbers of a cloud's half-edges + +## Main results + +- `Complexity.ConstraintGraph.idxOf_cloudList` — the position is a count +- `Complexity.ConstraintGraph.halfCode_getElem_cloudList` — and the entry at a + position is named by the count that reaches it +-/ + +@[expose] public section + +namespace Complexity + +namespace ConstraintGraph + +variable {α : Type} (G : ConstraintGraph α) + +/-- The numbers of the half-edges attached to a vertex. -/ +noncomputable def cloudCodes (v : Fin G.numVerts) : Finset ℕ := (G.cloud v).image G.halfCode + +@[simp] theorem mem_cloudCodes {v : Fin G.numVerts} {c : ℕ} : + c ∈ G.cloudCodes v ↔ ∃ p, G.owner p = v ∧ G.halfCode p = c := by + rw [cloudCodes, Finset.mem_image] + constructor + · rintro ⟨p, hp, rfl⟩ + exact ⟨p, (G.mem_cloud).mp hp, rfl⟩ + · rintro ⟨p, hp, rfl⟩ + exact ⟨p, (G.mem_cloud).mpr hp, rfl⟩ + +@[simp] theorem card_cloudCodes (v : Fin G.numVerts) : + (G.cloudCodes v).card = (G.cloud v).card := + Finset.card_image_of_injective _ G.halfCode_injective + +@[simp] theorem length_cloudList (v : Fin G.numVerts) : + (G.cloudList v).length = (G.cloud v).card := Finset.length_sort _ + +/-- Counting the smaller numbers of a cloud is counting its smaller +half-edges. -/ +theorem countBelow_cloudCodes (v : Fin G.numVerts) (c : ℕ) : + countBelow (G.cloudCodes v) c + = ((G.cloud v).filter fun q => G.halfCode q < c).card := by + classical + rw [countBelow, cloudCodes, Finset.filter_image] + exact Finset.card_image_of_injective _ G.halfCode_injective + +/-- **A half-edge's position in its cloud is a count.** -/ +theorem idxOf_cloudList {v : Fin G.numVerts} {p : G.HalfEdge} (hp : G.owner p = v) : + (G.cloudList v).idxOf p = countBelow (G.cloudCodes v) (G.halfCode p) := by + have hmem : p ∈ G.cloudList v := (G.mem_cloudList).mpr ((G.mem_cloud).mpr hp) + have hinj : ∀ x ∈ G.cloudList v, ∀ y ∈ G.cloudList v, G.halfCode x = G.halfCode y → x = y := + fun x _ y _ h => G.halfCode_injective h + have h1 : (G.cloudList v).idxOf p + = (G.cloudList v).countP (fun q => decide (G.halfCode q < G.halfCode p)) := + idxOf_eq_countP (G.pairwise_cloudList v) hinj hmem + rw [h1, countBelow_cloudCodes, List.countP_eq_length_filter] + have hnd : ((G.cloudList v).filter fun q => decide (G.halfCode q < G.halfCode p)).Nodup := + (G.nodup_cloudList v).filter _ + rw [← List.toFinset_card_of_nodup hnd] + congr 1 + ext q + simp only [List.mem_toFinset, List.mem_filter, Finset.mem_filter, mem_cloudList, + decide_eq_true_eq] + +theorem card_cloudCodes_eq_length (v : Fin G.numVerts) : + (G.cloudCodes v).card = (G.cloudList v).length := by + rw [card_cloudCodes, length_cloudList] + +/-- **The half-edge at a position is named by the count that reaches it.** -/ +theorem halfCode_getElem_cloudList (v : Fin G.numVerts) (k : ℕ) + (hk : k < (G.cloudList v).length) : + (G.cloudCodes v).orderEmbOfFin (G.card_cloudCodes_eq_length v) ⟨k, hk⟩ + = G.halfCode ((G.cloudList v)[k]) := by + classical + have hmem : (G.cloudList v)[k] ∈ G.cloudList v := List.getElem_mem hk + have howner : G.owner ((G.cloudList v)[k]) = v := + (G.mem_cloud).mp ((G.mem_cloudList).mp hmem) + have hcode : G.halfCode ((G.cloudList v)[k]) ∈ G.cloudCodes v := + (G.mem_cloudCodes).mpr ⟨_, howner, rfl⟩ + have hidx : (G.cloudList v).idxOf ((G.cloudList v)[k]) = k := + (G.nodup_cloudList v).idxOf_getElem _ hk + refine orderEmbOfFin_eq_of_countBelow (G.card_cloudCodes_eq_length v) _ hcode ?_ + rw [← G.idxOf_cloudList howner, hidx] + +end ConstraintGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/CloudDisagreement.lean b/Complexitylib/Classes/PCP/Internal/CloudDisagreement.lean new file mode 100644 index 00000000..68aa6eb1 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/CloudDisagreement.lean @@ -0,0 +1,264 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.DegreeReduction +public import Complexitylib.Classes.PCP.Internal.EdgeExpansion +public import Complexitylib.Classes.PCP.Internal.FinsetPlurality +public import Mathlib.Algebra.Order.Field.Basic + +/-! +# Charging disagreement inside a cloud + +The soundness half of Dinur's degree reduction, one cloud at a time. + +An assignment of the reduced system labels half-edges, not vertices. It is +decoded back to a vertex assignment by **plurality**: `decode A v` is a label +that at least a `1 / card α` fraction of `v`'s cloud agrees with. The +half-edges of the cloud that *disagree* form a set the cloud's expander must +charge for: by edge expansion, the disagreeing set sends out many cloud-links, +and every one of them joins two half-edges with different labels, so every one +of them is an unsatisfied constraint. + +Everything is phrased at the level of a cloud's *enumeration* `cloudList v`, so +that the expander family — which lives on `Fin n` — applies directly, with +`cloudRot_getElem` as the only bridge back to half-edges. + +## Main definitions + +- `ConstraintGraph.decode` — the plurality label of a cloud +- `ConstraintGraph.devIdx` — the indices of a cloud that disagree with it +- `ConstraintGraph.cloudUnsat` — the unsatisfied cloud-links sitting at a vertex + +## Main results + +- `ConstraintGraph.length_le_card_agree` — plurality: the agreeing part of a + cloud is at least a `1 / card α` fraction of it +- `ConstraintGraph.card_dartsBetween_le_card_cloudUnsat` — every expander + boundary dart of the disagreeing set is an unsatisfied cloud-link +- `ConstraintGraph.cloud_disagreement_bound` — the resulting charge: + `(1 - lam) · degree · |dev| / card α` unsatisfied links at `v` +-/ + +@[expose] public section + +namespace Complexity + +namespace ConstraintGraph + +variable {α : Type} [Fintype α] [DecidableEq α] [Nonempty α] +variable (G : ConstraintGraph α) (E : ExpanderFamily) + +/-! ### Plurality decoding -/ + +/-- The plurality label of `v`'s cloud: a label at least a `1 / card α` +fraction of the cloud agrees with. -/ +noncomputable def decode (A : G.HalfEdge → α) (v : Fin G.numVerts) : α := + Classical.choose (exists_plurality (Finset.univ : Finset (Fin (G.cloudList v).length)) + fun i => A (G.cloudList v)[i.val]) + +/-- The indices of `v`'s cloud whose labels disagree with the plurality. -/ +noncomputable def devIdx (A : G.HalfEdge → α) (v : Fin G.numVerts) : + Finset (Fin (G.cloudList v).length) := + Finset.univ.filter fun i => A (G.cloudList v)[i.val] ≠ G.decode A v + +theorem mem_devIdx {A : G.HalfEdge → α} {v : Fin G.numVerts} + {i : Fin (G.cloudList v).length} : + i ∈ G.devIdx A v ↔ A (G.cloudList v)[i.val] ≠ G.decode A v := by + simp [devIdx] + +theorem mem_compl_devIdx {A : G.HalfEdge → α} {v : Fin G.numVerts} + {i : Fin (G.cloudList v).length} : + i ∈ (G.devIdx A v)ᶜ ↔ A (G.cloudList v)[i.val] = G.decode A v := by + simp [devIdx] + +/-- **Plurality.** The part of a cloud agreeing with its decoded label is at +least a `1 / card α` fraction of the cloud. -/ +theorem length_le_card_agree (A : G.HalfEdge → α) (v : Fin G.numVerts) : + (G.cloudList v).length ≤ Fintype.card α * ((G.devIdx A v)ᶜ).card := by + have h := Classical.choose_spec + (exists_plurality (Finset.univ : Finset (Fin (G.cloudList v).length)) + fun i => A (G.cloudList v)[i.val]) + simpa [devIdx, decode, Finset.compl_filter, not_not] using h + +/-! ### From cloud indices to half-edges -/ + +omit [Fintype α] [DecidableEq α] [Nonempty α] in +/-- A cloud's filtered subset is the image of the corresponding index set. -/ +theorem cloud_filter_eq_image (v : Fin G.numVerts) (P : G.HalfEdge → Prop) [DecidablePred P] : + (G.cloud v).filter P + = (Finset.univ.filter fun i : Fin (G.cloudList v).length => + P (G.cloudList v)[i.val]).image fun i => (G.cloudList v)[i.val] := by + ext p + simp only [Finset.mem_filter, Finset.mem_image, Finset.mem_univ, true_and] + constructor + · rintro ⟨hp, hP⟩ + have hmem : p ∈ G.cloudList v := (G.mem_cloudList).mpr hp + have hlt : (G.cloudList v).idxOf p < (G.cloudList v).length := + List.idxOf_lt_length_iff.mpr hmem + have hget : (G.cloudList v)[(G.cloudList v).idxOf p] = p := List.getElem_idxOf hlt + exact ⟨⟨(G.cloudList v).idxOf p, hlt⟩, by rw [hget]; exact hP, hget⟩ + · rintro ⟨i, hP, rfl⟩ + exact ⟨(G.mem_cloudList).mp (List.getElem_mem i.isLt), hP⟩ + +omit [Fintype α] [DecidableEq α] [Nonempty α] in +/-- Counting inside a cloud is the same through its enumeration. -/ +theorem card_filter_cloud (v : Fin G.numVerts) (P : G.HalfEdge → Prop) [DecidablePred P] : + (Finset.univ.filter fun i : Fin (G.cloudList v).length => + P (G.cloudList v)[i.val]).card = ((G.cloud v).filter P).card := by + rw [G.cloud_filter_eq_image v P, Finset.card_image_of_injOn] + intro i _ j _ h + exact Fin.ext ((G.nodup_cloudList v).getElem_inj_iff.mp h) + +/-- The half-edges whose label disagrees with their vertex's decoded label. -/ +noncomputable def devSet (A : G.HalfEdge → α) : Finset G.HalfEdge := + Finset.univ.filter fun p => A p ≠ G.decode A (G.owner p) + +theorem mem_devSet {A : G.HalfEdge → α} {p : G.HalfEdge} : + p ∈ G.devSet A ↔ A p ≠ G.decode A (G.owner p) := by + simp [devSet] + +/-- A cloud's disagreeing indices count the same as its disagreeing +half-edges. -/ +theorem card_devIdx_eq (A : G.HalfEdge → α) (v : Fin G.numVerts) : + (G.devIdx A v).card = ((G.devSet A).filter fun p => G.owner p = v).card := by + have hset : (G.cloud v).filter (fun p => A p ≠ G.decode A v) + = (G.devSet A).filter fun p => G.owner p = v := by + ext p + simp only [Finset.mem_filter, mem_cloud, mem_devSet] + constructor + · rintro ⟨hv, hne⟩ + exact ⟨by rw [hv]; exact hne, hv⟩ + · rintro ⟨hne, hv⟩ + exact ⟨hv, by rw [← hv]; exact hne⟩ + rw [devIdx, G.card_filter_cloud v fun p => A p ≠ G.decode A v, hset] + +/-- Summed over the vertices, the cloud charges account for every disagreeing +half-edge exactly once. -/ +theorem sum_card_devIdx (A : G.HalfEdge → α) : + ∑ v : Fin G.numVerts, (G.devIdx A v).card = (G.devSet A).card := by + have h : (G.devSet A).card + = ∑ v : Fin G.numVerts, ((G.devSet A).filter fun p => G.owner p = v).card := + Finset.card_eq_sum_card_fiberwise fun p _ => Finset.mem_univ (G.owner p) + rw [h] + exact Finset.sum_congr rfl fun v _ => G.card_devIdx_eq A v + +/-! ### Unsatisfied cloud-links -/ + +/-- The unsatisfied cloud-links of the reduced system sitting at `v`. -/ +noncomputable def cloudUnsat (A : (G.reduce E).Assignment) (v : Fin G.numVerts) : + Finset (G.reduce E).Dart := + ((G.reduce E).unsatDarts A).filter fun x => G.owner x.1 = v ∧ x.2 ≠ none + +omit [Fintype α] [Nonempty α] in +/-- The clouds' unsatisfied links are disjoint: a dart's tail determines the +vertex it sits at. -/ +theorem cloudUnsat_disjoint (A : (G.reduce E).Assignment) {u v : Fin G.numVerts} (huv : u ≠ v) : + Disjoint (G.cloudUnsat E A u) (G.cloudUnsat E A v) := by + refine Finset.disjoint_left.mpr fun x hx hx' => ?_ + rw [cloudUnsat, Finset.mem_filter] at hx hx' + exact huv (hx.2.1.symm.trans hx'.2.1) + +/-- Every boundary dart of the disagreeing set is an unsatisfied cloud-link: +its two ends carry different labels, one being the plurality and one not. -/ +theorem card_dartsBetween_le_card_cloudUnsat (A : (G.reduce E).Assignment) + (v : Fin G.numVerts) : + ((E.graph (G.cloudList v).length).dartsBetween (G.devIdx A v) (G.devIdx A v)ᶜ).card + ≤ (G.cloudUnsat E A v).card := by + refine Finset.card_le_card_of_injOn + (fun x => ((G.cloudList v)[x.1.val], some x.2)) ?_ ?_ + · intro x hx + simp only [Finset.mem_coe] at hx ⊢ + rw [RegGraph.dartsBetween, Finset.mem_filter] at hx + obtain ⟨-, hx1, hx2⟩ := hx + have hnbr : (E.graph (G.cloudList v).length).nbr x.1 x.2 + = (E.rot (G.cloudList v).length (x.1, x.2)).1 := rfl + rw [hnbr] at hx2 + have hdev : A (G.cloudList v)[x.1.val] ≠ G.decode A v := (G.mem_devIdx).mp hx1 + have hagree : A (G.cloudList v)[(E.rot (G.cloudList v).length (x.1, x.2)).1.val] + = G.decode A v := (G.mem_compl_devIdx).mp hx2 + have howner : G.owner (G.cloudList v)[x.1.val] = v := + (G.mem_cloud).mp ((G.mem_cloudList).mp (List.getElem_mem x.1.isLt)) + rw [cloudUnsat, Finset.mem_filter] + refine ⟨?_, howner, by simp⟩ + rw [RegCSP.mem_unsatDarts] + show ¬ ((A (G.cloudList v)[x.1.val] + == A ((G.reduce E).graph.nbr (G.cloudList v)[x.1.val] (some x.2))) = true) + have hstep : (G.reduce E).graph.nbr (G.cloudList v)[x.1.val] (some x.2) + = (G.cloudList v)[(E.rot (G.cloudList v).length (x.1, x.2)).1.val] := by + show (G.cloudRot E (G.cloudList v)[x.1.val] x.2).1 = _ + rw [G.cloudRot_getElem E v x.1 x.2] + rw [hstep, hagree] + simpa using hdev + · intro x _ y _ hxy + have h1 : (G.cloudList v)[x.1.val] = (G.cloudList v)[y.1.val] := congrArg Prod.fst hxy + have h2 : x.2 = y.2 := by + have h := congrArg Prod.snd hxy + exact Option.some.inj h + have h3 : x.1 = y.1 := + Fin.ext ((G.nodup_cloudList v).getElem_inj_iff.mp h1) + exact Prod.ext h3 h2 + +/-! ### The charge -/ + +/-- **The cloud charge.** The disagreeing part of `v`'s cloud is billed +`(1 - lam) · degree / card α` unsatisfied cloud-links per disagreeing +half-edge. -/ +theorem cloud_disagreement_bound (A : (G.reduce E).Assignment) (v : Fin G.numVerts) : + (1 - E.lam) * (E.degree : ℝ) * ((G.devIdx A v).card : ℝ) / (Fintype.card α : ℝ) + ≤ ((G.cloudUnsat E A v).card : ℝ) := by + rcases Nat.eq_zero_or_pos (G.cloudList v).length with hlen | hlen + · have hempty : G.devIdx A v = ∅ := by + apply Finset.eq_empty_of_forall_notMem + intro i + exact absurd i.isLt (by omega) + rw [hempty] + simp + · have hordpos : 0 < (E.graph (G.cloudList v).length).order := by + rw [E.order_graph]; exact hlen + have hexp := (E.graph (G.cloudList v).length).card_dartsBetween_compl_ge + E.lam_nonneg (E.spectral_graph _) hordpos (G.devIdx A v) + rw [E.deg_graph, E.order_graph] at hexp + have hinj := G.card_dartsBetween_le_card_cloudUnsat E A v + have hinjR : (((E.graph (G.cloudList v).length).dartsBetween (G.devIdx A v) + (G.devIdx A v)ᶜ).card : ℝ) ≤ ((G.cloudUnsat E A v).card : ℝ) := by + exact_mod_cast hinj + refine le_trans ?_ (le_trans hexp hinjR) + -- plurality: `|devᶜ| ≥ length / card α` + have hplur : ((G.cloudList v).length : ℝ) + ≤ (Fintype.card α : ℝ) * (((G.devIdx A v)ᶜ).card : ℝ) := by + exact_mod_cast G.length_le_card_agree A v + have hlam : 0 ≤ 1 - E.lam := by linarith [E.lam_lt_one] + have hdev : (0 : ℝ) ≤ ((G.devIdx A v).card : ℝ) := by positivity + have hkey : ((G.devIdx A v).card : ℝ) / (Fintype.card α : ℝ) + ≤ ((G.devIdx A v).card : ℝ) * (((G.devIdx A v)ᶜ).card : ℝ) + / ((G.cloudList v).length : ℝ) := by + have hnum : ((G.devIdx A v).card : ℝ) * ((G.cloudList v).length : ℝ) + ≤ (((G.devIdx A v).card : ℝ) * (((G.devIdx A v)ᶜ).card : ℝ)) + * (Fintype.card α : ℝ) := by + nlinarith [hplur, hdev] + calc ((G.devIdx A v).card : ℝ) / (Fintype.card α : ℝ) + = (((G.devIdx A v).card : ℝ) * ((G.cloudList v).length : ℝ)) + / ((Fintype.card α : ℝ) * ((G.cloudList v).length : ℝ)) := by + field_simp + _ ≤ ((((G.devIdx A v).card : ℝ) * (((G.devIdx A v)ᶜ).card : ℝ)) + * (Fintype.card α : ℝ)) + / ((Fintype.card α : ℝ) * ((G.cloudList v).length : ℝ)) := by + gcongr + _ = ((G.devIdx A v).card : ℝ) * (((G.devIdx A v)ᶜ).card : ℝ) + / ((G.cloudList v).length : ℝ) := by + field_simp + calc (1 - E.lam) * (E.degree : ℝ) * ((G.devIdx A v).card : ℝ) / (Fintype.card α : ℝ) + = (1 - E.lam) * (E.degree : ℝ) + * (((G.devIdx A v).card : ℝ) / (Fintype.card α : ℝ)) := by ring + _ ≤ (1 - E.lam) * (E.degree : ℝ) + * (((G.devIdx A v).card : ℝ) * (((G.devIdx A v)ᶜ).card : ℝ) + / ((G.cloudList v).length : ℝ)) := by + have : (0 : ℝ) ≤ (1 - E.lam) * (E.degree : ℝ) := by positivity + exact mul_le_mul_of_nonneg_left hkey this + +end ConstraintGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/CoinEnum.lean b/Complexitylib/Classes/PCP/Internal/CoinEnum.lean new file mode 100644 index 00000000..6b41efd7 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/CoinEnum.lean @@ -0,0 +1,141 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P +public import Complexitylib.Classes.P.Cobham.Internal.BumpBits +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P.Cobham.Internal.HeadOps +public import Complexitylib.Classes.P.Cobham.Internal.BinValLE +public import Complexitylib.Classes.PCP.Internal.SubsetNP + +/-! +# Coin strings from their index + +A loop over the coin strings of a verifier receives its index in unary, since +that is the form a polynomial-time loop counter takes. This module turns such an +index into the coin string itself: the fixed-width binary counter of +`SavitchBits` is incremented that many times, starting from all zeros. + +Nothing here is arithmetic on the index. `bumpBits` is the width-preserving +increment already proved polynomial-time for Savitch's theorem, and iterating a +polynomial-time step a polynomial number of times is `iterate_mem_FP`. + +## Main results + +- `Complexity.coinStr` — the counter after that many increments +- `Complexity.coinStr_mem_FP` — in polynomial time, for any index +- `Complexity.toList_coinOfIndex` — it is the coin string `SubsetNP` names +-/ + +@[expose] public section + +namespace Complexity + +/-- A block of zeros as wide as a computed string. -/ +theorem zeroBlockFn_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => List.replicate (a z).length false) ∈ FP := + unFn_mem_FP (g := fun s => List.replicate s.length false) + (Cobham.zeroBlockFn (Cobham.proj 0)) ha + +theorem bumpBits_mem_FP : bumpBits ∈ FP := by + have h := bumpCodeFn_mem_FP id_mem_FP + refine mem_FP_of_eq h fun z => ?_ + simp + +theorem length_bumpBits_iterate (n : ℕ) (w : List Bool) : + (bumpBits^[n] w).length = w.length := by + induction n with + | zero => rfl + | succ n ih => rw [Function.iterate_succ_apply', bumpBits_length, ih] + +/-- The width-`t` counter after `c` increments. Total: past `2 ^ t` it wraps, +which never happens where it is used but keeps the function unconditional. -/ +def coinStr (t c : ℕ) : List Bool := bumpBits^[c] (List.replicate t false) + +theorem coinStr_eq {t c : ℕ} (h : c < 2 ^ t) : coinStr t c = bitsOfLenLE t c := by + rw [coinStr, ← bitsOfLenLE_zero t, bumpBits_iterate _ _ h] + +/-- **The counter value in polynomial time.** With the width and the index both +supplied in unary, the counter is polynomial-time computable — with no bound on +the index, so that the function is total where a loop guard has not yet been +applied. -/ +theorem coinStr_mem_FP {t c : List Bool → ℕ} + (ht : (fun z => List.replicate (t z) true) ∈ FP) + (hc : (fun z => List.replicate (c z) true) ∈ FP) : + (fun z => coinStr (t z) (c z)) ∈ FP := by + have hinit : (fun z => List.replicate (t z) false) ∈ FP := by + have := zeroBlockFn_mem_FP ht + simpa using this + have hbound : ∀ z : List Bool, ∀ n ≤ (List.replicate (c z) true).length, + (bumpBits^[n] (List.replicate (t z) false)).length + ≤ (List.replicate (t z) false).length := fun z n _ => + le_of_eq (length_bumpBits_iterate n _) + have hiter := Cobham.iterate_mem_FP bumpBits_mem_FP hinit hc hinit hbound + refine mem_FP_of_eq hiter fun z => ?_ + rw [List.length_replicate, coinStr] + +/-- **The counter is the coin string.** `SubsetNP` indexes coin strings by their +little-endian binary value, which is exactly what the counter holds. -/ +theorem toList_coinOfIndex (t c : ℕ) (h : c < 2 ^ t) : + BitString.toList (PCPVerifier.coinOfIndex (t := t) ⟨c, h⟩) = bitsOfLenLE t c := by + refine List.ext_getElem (by simp) fun j h1 h2 => ?_ + have hj : j < t := by simpa using h1 + rw [bitsOfLenLE_getElem t c j hj, + BitString.getElem_toList (PCPVerifier.coinOfIndex (t := t) ⟨c, h⟩) ⟨j, hj⟩, + PCPVerifier.coinOfIndex] + have hval := finFunctionFinEquiv_symm_apply_val (⟨c, h⟩ : Fin (2 ^ t)) (⟨j, hj⟩ : Fin t) + have h2 : (finFunctionFinEquiv.symm (⟨c, h⟩ : Fin (2 ^ t)) ⟨j, hj⟩ = 1) + ↔ (c / 2 ^ j % 2 = 1) := by + rw [Fin.ext_iff, hval] + exact Iff.rfl + exact decide_eq_decide.mpr h2 + +/-- The index of the coin string an index names. -/ +theorem coinIndex_coinOfIndex {t : ℕ} (c : Fin (2 ^ t)) : + PCPVerifier.coinIndex (PCPVerifier.coinOfIndex c) = c.val := by + have hd : PCPVerifier.coinDigits (PCPVerifier.coinOfIndex c) + = finFunctionFinEquiv.symm c := by + funext i + rw [PCPVerifier.coinDigits, PCPVerifier.coinOfIndex] + have hv : (finFunctionFinEquiv.symm c i).val = 0 + ∨ (finFunctionFinEquiv.symm c i).val = 1 := by omega + rcases hv with hv | hv + · have h0 : finFunctionFinEquiv.symm c i = 0 := Fin.ext hv + simp [h0] + · have h1 : finFunctionFinEquiv.symm c i = 1 := Fin.ext hv + simp [h1] + rw [PCPVerifier.coinIndex, hd, Equiv.apply_symm_apply] + +/-- **The value of a coin string is its index.** -/ +theorem binValLE_toList {T : ℕ} (ρ : Fin T → Bool) : + binValLE (BitString.toList ρ) = PCPVerifier.coinIndex ρ := by + have hlt : PCPVerifier.coinIndex ρ < 2 ^ T := PCPVerifier.coinIndex_lt ρ + have hρ : BitString.toList ρ = bitsOfLenLE T (PCPVerifier.coinIndex ρ) := by + rw [← PCPVerifier.coinOfIndex_coinIndex ρ hlt, toList_coinOfIndex] + congr 1 + rw [PCPVerifier.coinOfIndex_coinIndex ρ hlt] + rw [hρ, binValLE_bitsOfLenLE _ _ hlt] + +/-- **Counting coin strings is counting indices.** -/ +theorem card_filter_coinIndex (T : ℕ) (Q : ℕ → Prop) [DecidablePred Q] : + (Finset.univ.filter (fun ρ : Fin T → Bool => Q (PCPVerifier.coinIndex ρ))).card + = ((Finset.range (2 ^ T)).filter Q).card := by + classical + refine Finset.card_bij (fun ρ _ => PCPVerifier.coinIndex ρ) ?_ ?_ ?_ + · intro ρ hρ + simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hρ + exact Finset.mem_filter.mpr ⟨Finset.mem_range.mpr (PCPVerifier.coinIndex_lt ρ), hρ⟩ + · intro ρ₁ h₁ ρ₂ h₂ heq + exact PCPVerifier.coinIndex_injective heq + · intro c hc + rw [Finset.mem_filter, Finset.mem_range] at hc + refine ⟨PCPVerifier.coinOfIndex ⟨c, hc.1⟩, ?_, ?_⟩ + · simp only [Finset.mem_filter, Finset.mem_univ, true_and] + rw [coinIndex_coinOfIndex] + exact hc.2 + · exact coinIndex_coinOfIndex ⟨c, hc.1⟩ + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/Compose.lean b/Complexitylib/Classes/PCP/Internal/Compose.lean new file mode 100644 index 00000000..37dfced7 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/Compose.lean @@ -0,0 +1,415 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.TesterCore +public import Complexitylib.Classes.PCP.Internal.LocalTest +public import Complexitylib.Classes.PCP.Internal.RegCSP + +/-! +# Dinur's composition: alphabet reduction + +The powering step leaves a constraint graph over an enormous (though constant) +alphabet. Composition brings the alphabet back down to a fixed one, at the cost +of a constant factor in the unsatisfiability value that does *not* depend on the +alphabet being reduced. That independence is what lets the powering step's gain +win. + +The construction: every vertex of the outer graph gets a block of positions +holding the **Hadamard encoding** of its label; every dart gets a proof for the +assembled tester of `TesterCore`, whose input tables are the encodings at its +two ends and whose constraint is the dart's relation, spelled out on encoded +pairs. The tester's reads make a `MultiTest`, and `LocalTest` turns it into a +binary constraint graph over the fixed alphabet `Alpha ReadIdx`. + +Soundness decodes an assignment of the composed graph to one of the outer graph +by nearest codeword at every vertex. Whenever the decoded assignment violates a +dart, that dart's tester rejects on a `1/32` fraction of its random strings — +otherwise the tester's own soundness would produce a satisfying pair whose +encodings are close to both blocks, and closeness to a codeword pins the +decoded labels down. Each rejecting string costs one of the `22` edges it owns. + +## Main definitions + +- `Complexity.ReadIdx` — the tester's `22` reads +- `Complexity.RegCSP.compose` — the composed `MultiTest` +- `Complexity.RegCSP.decodeAssign` — decoding an assignment of the composed + graph + +## Main results + +- `Complexity.RegCSP.le_unsatVal_compose` — the value drops by at most `704` +- `Complexity.RegCSP.satisfiable_compose` — satisfiability is preserved +-/ + +@[expose] public section + +namespace Complexity + +open BooleanAnalysis + +/-! ### The reads -/ + +/-- The tester's reads, by check: linearity of `F` (three), linearity of `G` +(three), consistency (six), the constraint (four), and the two input tables +(three each: the input coordinate and two self-correction reads). -/ +inductive ReadIdx + | f1x | f1y | f1s + | g2x | g2y | g2s + | c3cQ | c3tQ | c3cX | c3xX | c3cY | c3yY + | k4qG | k4tG | k4cF | k4lF + | i5r | i5c | i5b + | i6r | i6c | i6b + deriving DecidableEq, Fintype + +theorem card_readIdx : Fintype.card ReadIdx = 22 := rfl + +instance : Nonempty ReadIdx := ⟨ReadIdx.f1x⟩ + +/-- The reads are numbered by their own enumeration: there are twenty-two of +them, so this is a lookup on a bounded key. -/ +noncomputable instance : NumEnc ReadIdx := NumEnc.ofFintype _ + +/-! ### Signs and bits -/ + +theorem chi_eq_chi_iff (u v : ZMod 2) : chi u = chi v ↔ u = v := by + constructor + · intro h + have h' := signBit_chi u + rw [h, signBit_chi] at h' + exact h'.symm + · intro h + rw [h] + +theorem signBit_signOf_mul {m : ℕ} (F : Cube m → ZMod 2) (u v : Cube m) : + signBit (signOf F u * signOf F v) = F u + F v := by + show signBit (chi (F u) * chi (F v)) = _ + rw [← BooleanAnalysis.Internal.chi_add, signBit_chi] + +theorem signOf_mul_eq_iff {m : ℕ} (F : Cube m → ZMod 2) (u v w : Cube m) : + signOf F u * signOf F v = signOf F w ↔ F u + F v = F w := by + show chi (F u) * chi (F v) = chi (F w) ↔ _ + rw [← BooleanAnalysis.Internal.chi_add, chi_eq_chi_iff] + +/-! ### The checks as a formula on the bits read -/ + +namespace Tester + +variable {B : ℕ} + +/-- The tester's verdict as a formula on the bits it read. -/ +noncomputable def bitFormula (S : Finset (Cube (kOf B))) (z : Cube (ROf B)) + (rd : ReadIdx → ZMod 2) : Prop := + rd .f1x + rd .f1y = rd .f1s + ∧ rd .g2x + rd .g2y = rd .g2s + ∧ rd .c3cQ + rd .c3tQ = (rd .c3cX + rd .c3xX) * (rd .c3cY + rd .c3yY) + ∧ (rd .k4qG + rd .k4tG) + (rd .k4cF + rd .k4lF) + + (QuadConstraint.combine (oneHotSystem S) (leftBlock (blk4 z))).const = 0 + ∧ rd .i5c + rd .i5b = rd .i5r + ∧ rd .i6c + rd .i6b = rd .i6r + +noncomputable instance (S : Finset (Cube (kOf B))) (z : Cube (ROf B)) + (rd : ReadIdx → ZMod 2) : Decidable (bitFormula S z rd) := by + unfold bitFormula + infer_instance + +/-- The bits the tester reads from the four tables. -/ +noncomputable def readsOf (Tt Th : Cube B → ZMod 2) (F : Cube (nOf B) → ZMod 2) + (G : Cube (nOf B * nOf B) → ZMod 2) (S : Finset (Cube (kOf B))) (z : Cube (ROf B)) : + ReadIdx → ZMod 2 + | .f1x => F (leftBlock (blk1 z)) + | .f1y => F (rightBlock (blk1 z)) + | .f1s => F (leftBlock (blk1 z) + rightBlock (blk1 z)) + | .g2x => G (leftBlock (blk2 z)) + | .g2y => G (rightBlock (blk2 z)) + | .g2s => G (leftBlock (blk2 z) + rightBlock (blk2 z)) + | .c3cQ => G (cQ (blk3 z)) + | .c3tQ => G (tensor (qX (blk3 z)) (qY (blk3 z)) + cQ (blk3 z)) + | .c3cX => F (cX (blk3 z)) + | .c3xX => F (qX (blk3 z) + cX (blk3 z)) + | .c3cY => F (cY (blk3 z)) + | .c3yY => F (qY (blk3 z) + cY (blk3 z)) + | .k4qG => G (rightBlock (rightBlock (blk4 z))) + | .k4tG => G ((QuadConstraint.combine (oneHotSystem S) (leftBlock (blk4 z))).quad + + rightBlock (rightBlock (blk4 z))) + | .k4cF => F (leftBlock (rightBlock (blk4 z))) + | .k4lF => F ((QuadConstraint.combine (oneHotSystem S) (leftBlock (blk4 z))).lin + + leftBlock (rightBlock (blk4 z))) + | .i5r => Tt (leftBlock (blk5 z)) + | .i5c => F (rightBlock (blk5 z)) + | .i5b => F (basisVec (inTail B (leftBlock (blk5 z))) + rightBlock (blk5 z)) + | .i6r => Th (leftBlock (blk6 z)) + | .i6c => F (rightBlock (blk6 z)) + | .i6b => F (basisVec (inHead B (leftBlock (blk6 z))) + rightBlock (blk6 z)) + +/-- **The verdict is the formula on the reads.** -/ +theorem allChecks_iff (S : Finset (Cube (kOf B))) (Tt Th : Cube B → ZMod 2) + (F : Cube (nOf B) → ZMod 2) (G : Cube (nOf B * nOf B) → ZMod 2) (z : Cube (ROf B)) : + AllChecks S Tt Th F G z ↔ bitFormula S z (readsOf Tt Th F G S z) := by + unfold AllChecks bitFormula LinCheck TesterAccepts ConstraintAccepts CoordAccepts + simp only [readsOf, signOf_mul_eq_iff, signBit_signOf_mul] + +end Tester + +/-! ### The composition -/ + +namespace RegCSP + +open Tester + +variable {β : Type} {B : ℕ} (enc : β → Cube B) + +/-- A bit table on `Cube B`, as a vector indexed by `Fin (2 ^ B)`. -/ +noncomputable def vecOf (t : Cube B → ZMod 2) : Cube (2 ^ B) := fun m => t ((candIdx B).symm m) + +/-- The tester's input variables for a pair of labels: the two encodings. -/ +noncomputable def inputVec (σ τ : β) : Cube (kOf B) := + Fin.append (vecOf (hadamard (enc σ))) (vecOf (hadamard (enc τ))) + +theorem tailPart_of_leftBlock (σ τ : β) (a : Cube (nOf B)) + (h : leftBlock a = inputVec enc σ τ) : tailPart a = hadamard (enc σ) := by + funext r + have hr : tailPart a r = leftBlock (leftBlock a) (candIdx B r) := rfl + rw [hr, h] + show (Fin.append (vecOf (hadamard (enc σ))) (vecOf (hadamard (enc τ)))) + (Fin.castAdd _ (candIdx B r)) = _ + rw [Fin.append_left] + show hadamard (enc σ) ((candIdx B).symm (candIdx B r)) = _ + rw [Equiv.symm_apply_apply] + +theorem headPart_of_leftBlock (σ τ : β) (a : Cube (nOf B)) + (h : leftBlock a = inputVec enc σ τ) : headPart a = hadamard (enc τ) := by + funext r + have hr : headPart a r = leftBlock a (Fin.natAdd (2 ^ B) (candIdx B r)) := rfl + rw [hr, h] + show (Fin.append (vecOf (hadamard (enc σ))) (vecOf (hadamard (enc τ)))) + (Fin.natAdd _ (candIdx B r)) = _ + rw [Fin.append_right] + show hadamard (enc τ) ((candIdx B).symm (candIdx B r)) = _ + rw [Equiv.symm_apply_apply] + +variable [Fintype β] [DecidableEq β] [Nonempty β] (R : RegCSP β) + [NumEnc R.graph.V] [NumEnc R.graph.D] + +/-- The satisfying set of a dart: the encoded pairs its relation accepts. -/ +noncomputable def satSet (p : R.Dart) : Finset (Cube (kOf B)) := + (Finset.univ.filter fun st : β × β => R.rel p.1 p.2 st.1 st.2 = true).image + fun st => inputVec enc st.1 st.2 + +omit [DecidableEq β] [Nonempty β] [NumEnc R.graph.V] [NumEnc R.graph.D] in +theorem mem_satSet_iff (p : R.Dart) (w : Cube (kOf B)) : + w ∈ R.satSet enc p ↔ ∃ σ τ, R.rel p.1 p.2 σ τ = true ∧ inputVec enc σ τ = w := by + simp only [satSet, Finset.mem_image, Finset.mem_filter, Finset.mem_univ, true_and, + Prod.exists] + +/-- The positions of the composed proof: an encoding block per vertex, and a +linear and a quadratic table per dart. -/ +abbrev Pos : Type := + (R.graph.V × Cube B) ⊕ ((R.Dart × Cube (nOf B)) ⊕ (R.Dart × Cube (nOf B * nOf B))) + +/-- **The composed test**: for each dart, the assembled tester on the encodings +at its ends and its own proof tables. -/ +noncomputable def compose : MultiTest (R.Pos (B := B)) R.Dart ReadIdx where + R := ROf B + pos := fun p z i => + match i with + | .f1x => Sum.inr (Sum.inl (p, leftBlock (blk1 z))) + | .f1y => Sum.inr (Sum.inl (p, rightBlock (blk1 z))) + | .f1s => Sum.inr (Sum.inl (p, leftBlock (blk1 z) + rightBlock (blk1 z))) + | .g2x => Sum.inr (Sum.inr (p, leftBlock (blk2 z))) + | .g2y => Sum.inr (Sum.inr (p, rightBlock (blk2 z))) + | .g2s => Sum.inr (Sum.inr (p, leftBlock (blk2 z) + rightBlock (blk2 z))) + | .c3cQ => Sum.inr (Sum.inr (p, cQ (blk3 z))) + | .c3tQ => Sum.inr (Sum.inr (p, tensor (qX (blk3 z)) (qY (blk3 z)) + cQ (blk3 z))) + | .c3cX => Sum.inr (Sum.inl (p, cX (blk3 z))) + | .c3xX => Sum.inr (Sum.inl (p, qX (blk3 z) + cX (blk3 z))) + | .c3cY => Sum.inr (Sum.inl (p, cY (blk3 z))) + | .c3yY => Sum.inr (Sum.inl (p, qY (blk3 z) + cY (blk3 z))) + | .k4qG => Sum.inr (Sum.inr (p, rightBlock (rightBlock (blk4 z)))) + | .k4tG => Sum.inr (Sum.inr (p, + (QuadConstraint.combine (oneHotSystem (R.satSet enc p)) (leftBlock (blk4 z))).quad + + rightBlock (rightBlock (blk4 z)))) + | .k4cF => Sum.inr (Sum.inl (p, leftBlock (rightBlock (blk4 z)))) + | .k4lF => Sum.inr (Sum.inl (p, + (QuadConstraint.combine (oneHotSystem (R.satSet enc p)) (leftBlock (blk4 z))).lin + + leftBlock (rightBlock (blk4 z)))) + | .i5r => Sum.inl (p.1, leftBlock (blk5 z)) + | .i5c => Sum.inr (Sum.inl (p, rightBlock (blk5 z))) + | .i5b => Sum.inr (Sum.inl (p, basisVec (inTail B (leftBlock (blk5 z))) + rightBlock (blk5 z))) + | .i6r => Sum.inl (R.graph.nbr p.1 p.2, leftBlock (blk6 z)) + | .i6c => Sum.inr (Sum.inl (p, rightBlock (blk6 z))) + | .i6b => Sum.inr (Sum.inl (p, basisVec (inHead B (leftBlock (blk6 z))) + rightBlock (blk6 z))) + check := fun p z rd => decide (bitFormula (R.satSet enc p) z rd) + +/-- The encoding block at a dart's tail, as the tester's first input table. -/ +noncomputable def tailTable (T : MultiTest.Table (R.Pos (B := B))) (p : R.Dart) : + Cube B → ZMod 2 := fun r => T (Sum.inl (p.1, r)) +/-- The encoding block at a dart's head, as the tester's second input table. -/ +noncomputable def headTable (T : MultiTest.Table (R.Pos (B := B))) (p : R.Dart) : + Cube B → ZMod 2 := fun r => T (Sum.inl (R.graph.nbr p.1 p.2, r)) +/-- A dart's linear proof table. -/ +noncomputable def linTable (T : MultiTest.Table (R.Pos (B := B))) (p : R.Dart) : + Cube (nOf B) → ZMod 2 := fun x => T (Sum.inr (Sum.inl (p, x))) +/-- A dart's quadratic proof table. -/ +noncomputable def quadTable (T : MultiTest.Table (R.Pos (B := B))) (p : R.Dart) : + Cube (nOf B * nOf B) → ZMod 2 := fun y => T (Sum.inr (Sum.inr (p, y))) + +omit [DecidableEq β] [Nonempty β] [NumEnc R.graph.V] [NumEnc R.graph.D] in +/-- **The composed test runs the tester.** -/ +theorem accepts_compose_iff (T : MultiTest.Table (R.Pos (B := B))) (p : R.Dart) + (z : Cube (ROf B)) : + (R.compose enc).accepts T p z = true + ↔ AllChecks (R.satSet enc p) (R.tailTable T p) (R.headTable T p) (R.linTable T p) + (R.quadTable T p) z := by + rw [allChecks_iff] + show decide (bitFormula (R.satSet enc p) z fun i => T ((R.compose enc).pos p z i)) = true ↔ _ + rw [decide_eq_true_iff] + have hreads : (fun i => T ((R.compose enc).pos p z i)) + = readsOf (R.tailTable T p) (R.headTable T p) (R.linTable T p) (R.quadTable T p) + (R.satSet enc p) z := by + funext i + cases i <;> rfl + rw [hreads] + +/-! ### Soundness -/ + +/-- Decoding an assignment of the composed graph: nearest codeword at each +vertex. -/ +noncomputable def decodeAssign (A : (R.compose enc).toGraph.Assignment) : R.Assignment := + fun v => decodeLabel enc fun r => (R.compose enc).tableOf A (Sum.inl (v, r)) + +omit [DecidableEq β] in +/-- **A violated dart rejects a `1/32` fraction of its random strings.** -/ +theorem card_rejects_ge (henc : Function.Injective enc) + (A : (R.compose enc).toGraph.Assignment) (p : R.Dart) + (hp : ¬ R.Satisfies (R.decodeAssign enc A) p) : + 2 ^ ROf B ≤ 32 * ((R.compose enc).rejects ((R.compose enc).tableOf A) p).card := by + classical + by_contra hlt + push Not at hlt + set T := (R.compose enc).tableOf A with hT + have hprob : 1 - 1 / 32 < Pr[AllChecks (R.satSet enc p) (R.tailTable T p) (R.headTable T p) + (R.linTable T p) (R.quadTable T p)] := by + have hacc : Pr[fun z : Cube (ROf B) => (R.compose enc).accepts T p z = true] + = 1 - (((R.compose enc).rejects T p).card : ℝ) / 2 ^ ROf B := + (R.compose enc).prob_accepts_eq T p + have heq : (fun z : Cube (ROf B) => (R.compose enc).accepts T p z = true) + = AllChecks (R.satSet enc p) (R.tailTable T p) (R.headTable T p) + (R.linTable T p) (R.quadTable T p) := by + funext z + exact propext (R.accepts_compose_iff enc T p z) + rw [heq] at hacc + rw [hacc] + have hlt' : (32 : ℝ) * ((R.compose enc).rejects T p).card < 2 ^ ROf B := by + exact_mod_cast hlt + have hpos : (0 : ℝ) < 2 ^ ROf B := by positivity + rw [sub_lt_sub_iff_left, div_lt_iff₀ hpos] + linarith + obtain ⟨a, hsys, hdt, hdh⟩ := Tester.sound _ _ _ _ _ hprob + have hmem := mem_of_sat_oneHotSystem _ a hsys + rw [mem_satSet_iff] at hmem + obtain ⟨σ, τ, hrel, heq⟩ := hmem + have htail := tailPart_of_leftBlock enc σ τ a heq.symm + have hhead := headPart_of_leftBlock enc σ τ a heq.symm + rw [htail] at hdt + rw [hhead] at hdh + have hdt' : bitDist (R.tailTable T p) (hadamard (enc σ)) < 1 / 4 := by linarith + have hdh' : bitDist (R.headTable T p) (hadamard (enc τ)) < 1 / 4 := by linarith + have hσ : R.decodeAssign enc A p.1 = σ := decodeLabel_eq enc henc _ σ hdt' + have hτ : R.decodeAssign enc A (R.graph.nbr p.1 p.2) = τ := decodeLabel_eq enc henc _ τ hdh' + apply hp + show R.rel p.1 p.2 (R.decodeAssign enc A p.1) (R.decodeAssign enc A (R.graph.nbr p.1 p.2)) = true + rw [hσ, hτ] + exact hrel + +omit [DecidableEq β] in +/-- **Soundness of composition**, per assignment: the composed graph's violated +fraction is at least the decoded assignment's, divided by `704 = 32 · 22`. -/ +theorem unsatFrac_compose_ge (henc : Function.Injective enc) + (A : (R.compose enc).toGraph.Assignment) : + R.unsatFrac (R.decodeAssign enc A) / 704 ≤ (R.compose enc).toGraph.unsatFrac A := by + classical + refine le_trans ?_ ((R.compose enc).unsatFrac_toGraph_ge A) + rw [card_readIdx] + set T := (R.compose enc).tableOf A + have hsum : ((R.unsatDarts (R.decodeAssign enc A)).card : ℚ) * 2 ^ ROf B + ≤ 32 * ∑ p : R.Dart, (((R.compose enc).rejects T p).card : ℚ) := by + have h1 : ∑ p ∈ R.unsatDarts (R.decodeAssign enc A), ((2 : ℚ) ^ ROf B) + ≤ ∑ p ∈ R.unsatDarts (R.decodeAssign enc A), + 32 * (((R.compose enc).rejects T p).card : ℚ) := by + refine Finset.sum_le_sum fun p hp => ?_ + rw [mem_unsatDarts] at hp + exact_mod_cast R.card_rejects_ge enc henc A p hp + have h2 : ∑ p ∈ R.unsatDarts (R.decodeAssign enc A), + 32 * (((R.compose enc).rejects T p).card : ℚ) + ≤ ∑ p : R.Dart, 32 * (((R.compose enc).rejects T p).card : ℚ) := + Finset.sum_le_sum_of_subset_of_nonneg (Finset.subset_univ _) + fun p _ _ => by positivity + rw [Finset.sum_const, nsmul_eq_mul, ← Finset.mul_sum] at h1 + rw [← Finset.mul_sum, ← Finset.mul_sum] at h2 + linarith + have hE : (Fintype.card R.Dart : ℚ) = ((R.graph.order * R.graph.deg : ℕ) : ℚ) := by + rw [R.card_dart] + rw [RegCSP.unsatFrac, hE] + rcases Nat.eq_zero_or_pos (R.graph.order * R.graph.deg) with h0 | h0 + · rw [h0] + simp + · have hE' : (0 : ℚ) < ((R.graph.order * R.graph.deg : ℕ) : ℚ) := by exact_mod_cast h0 + rw [div_div, div_le_div_iff₀ (by positivity) (by positivity)] + have key := mul_le_mul_of_nonneg_right hsum + (by positivity : (0 : ℚ) ≤ 22 * ((R.graph.order * R.graph.deg : ℕ) : ℚ)) + have key' : ((R.unsatDarts (R.decodeAssign enc A)).card : ℚ) * 2 ^ (R.compose enc).R + * (22 * ((R.graph.order * R.graph.deg : ℕ) : ℚ)) + ≤ (32 * ∑ p : R.Dart, (((R.compose enc).rejects T p).card : ℚ)) + * (22 * ((R.graph.order * R.graph.deg : ℕ) : ℚ)) := key + push_cast at key' ⊢ + linarith [key'] + +omit [DecidableEq β] in +/-- **Soundness of composition.** -/ +theorem le_unsatVal_compose (henc : Function.Injective enc) : + R.unsatVal / 704 ≤ (R.compose enc).toGraph.unsatVal := by + refine ConstraintGraph.le_unsatVal fun A => ?_ + refine le_trans ?_ (R.unsatFrac_compose_ge enc henc A) + have := R.unsatVal_le (R.decodeAssign enc A) + linarith + +/-! ### Completeness -/ + +/-- The honest proof of a satisfying assignment: encodings at the vertices, and +for each dart the Hadamard tables of the one-hot extension of its encoded pair. +-/ +noncomputable def honestTable (σ : R.Assignment) : MultiTest.Table (R.Pos (B := B)) + | Sum.inl (v, r) => hadamard (enc (σ v)) r + | Sum.inr (Sum.inl (p, x)) => + hadamard (oneHotExtend (inputVec enc (σ p.1) (σ (R.graph.nbr p.1 p.2)))) x + | Sum.inr (Sum.inr (p, y)) => + hadamard (tensorAssign (oneHotExtend (inputVec enc (σ p.1) (σ (R.graph.nbr p.1 p.2))))) y + +omit [DecidableEq β] [Nonempty β] in +/-- **Completeness of composition.** -/ +theorem satisfiable_compose (h : R.Satisfiable) : (R.compose enc).toGraph.Satisfiable := by + obtain ⟨σ, hσ⟩ := h + refine (R.compose enc).satisfiable_toGraph (R.honestTable enc σ) fun p z => ?_ + rw [accepts_compose_iff] + set a := oneHotExtend (inputVec enc (σ p.1) (σ (R.graph.nbr p.1 p.2))) with ha + have hleft : leftBlock a = inputVec enc (σ p.1) (σ (R.graph.nbr p.1 p.2)) := + leftBlock_oneHotExtend _ + have htail : R.tailTable (R.honestTable enc σ) p = tailPart a := by + rw [tailPart_of_leftBlock enc _ _ a hleft] + rfl + have hhead : R.headTable (R.honestTable enc σ) p = headPart a := by + rw [headPart_of_leftBlock enc _ _ a hleft] + rfl + have hlin : R.linTable (R.honestTable enc σ) p = hadamard a := rfl + have hquad : R.quadTable (R.honestTable enc σ) p = hadamard (tensorAssign a) := rfl + rw [htail, hhead, hlin, hquad] + refine Tester.complete _ a (sat_oneHotSystem_extend _ _ ?_) z + rw [mem_satSet_iff] + exact ⟨σ p.1, σ (R.graph.nbr p.1 p.2), hσ p, rfl⟩ + +end RegCSP + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/ConsistencyLang.lean b/Complexitylib/Classes/PCP/Internal/ConsistencyLang.lean new file mode 100644 index 00000000..99b184dc --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/ConsistencyLang.lean @@ -0,0 +1,476 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.HeadOps +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P.DecisionFn +public import Complexitylib.Classes.PCP.Internal.VerifierLang + +/-! +# Checking that a witness is consistent + +A witness records an answer for every query slot. It comes from an actual proof +only if slots reading the same proof position carry the same answer, and that is +what this module checks: four nested loops over a pair of coin strings and a +pair of query indices. + +The check never looks at a position as a number. `posAt` returns each query +position as its own serialization, so slots reading the same position are +recognised by comparing strings — and a slot past the end of a query list is +recognised by that string being empty. + +## Main definitions + +- `Complexity.consLang` — the consistency check, as a language of `pair x w` + +## Main results + +- `Complexity.consLang_mem_P` — the check is polynomial time +-/ + +@[expose] public section + +namespace Complexity + +section Consistency + +variable (V : PCPVerifier) (f : List Bool → List Bool) (r : ℕ → ℕ) (Q : ℕ) + +/-! ### Reading the nested loop input + +The innermost input is `pair (pair (pair (pair (pair x w) ρ) ρ') i) i'`, with +the four loop indices in unary. -/ + +/-- Strip the last two indices. -/ +def conY2 (y : List Bool) : List Bool := pairFst (pairFst y) + +/-- Strip the last three indices. -/ +def conY1 (y : List Bool) : List Bool := pairFst (conY2 y) + +/-- The original `pair x w`. -/ +def conY0 (y : List Bool) : List Bool := pairFst (conY1 y) + +/-- The input. -/ +def conX (y : List Bool) : List Bool := pairFst (conY0 y) + +/-- The witness. -/ +def conW (y : List Bool) : List Bool := pairSnd (conY0 y) + +/-- The first coin index. -/ +def conC1 (y : List Bool) : ℕ := (pairSnd (conY1 y)).length + +/-- The second coin index. -/ +def conC2 (y : List Bool) : ℕ := (pairSnd (conY2 y)).length + +/-- The first query index. -/ +def conC3 (y : List Bool) : ℕ := (pairSnd (pairFst y)).length + +/-- The second query index. -/ +def conC4 (y : List Bool) : ℕ := (pairSnd y).length + +theorem conY2_mem_FP : conY2 ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + +theorem conY1_mem_FP : conY1 ∈ FP := + mem_FP_comp conY2_mem_FP Cobham.fstBlock_mem_FP + +theorem conY0_mem_FP : conY0 ∈ FP := + mem_FP_comp conY1_mem_FP Cobham.fstBlock_mem_FP + +theorem conX_mem_FP : conX ∈ FP := + mem_FP_comp conY0_mem_FP Cobham.fstBlock_mem_FP + +theorem conW_mem_FP : conW ∈ FP := + mem_FP_comp conY0_mem_FP Cobham.sndBlock_mem_FP + +theorem unary_conC1_mem_FP : (fun y => List.replicate (conC1 y) true) ∈ FP := by + have := mem_FP_comp (mem_FP_comp conY1_mem_FP Cobham.sndBlock_mem_FP) unaryLength_mem_FP + simpa using this + +theorem unary_conC2_mem_FP : (fun y => List.replicate (conC2 y) true) ∈ FP := by + have := mem_FP_comp (mem_FP_comp conY2_mem_FP Cobham.sndBlock_mem_FP) unaryLength_mem_FP + simpa using this + +theorem unary_conC3_mem_FP : (fun y => List.replicate (conC3 y) true) ∈ FP := by + have := mem_FP_comp + (mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP) unaryLength_mem_FP + simpa using this + +theorem unary_conC4_mem_FP : (fun y => List.replicate (conC4 y) true) ∈ FP := by + have := mem_FP_comp Cobham.sndBlock_mem_FP unaryLength_mem_FP + simpa using this + +/-! ### The check -/ + +/-- The first coin string. -/ +noncomputable def conRho (y : List Bool) : List Bool := + coinStr (r (conX y).length) (conC1 y) + +/-- The second coin string. -/ +noncomputable def conRho' (y : List Bool) : List Bool := + coinStr (r (conX y).length) (conC2 y) + +/-- The position the first slot queries, as a string. -/ +noncomputable def conP (y : List Bool) : List Bool := + posAt (f (pair (conX y) (conRho r y))) (conC3 y) + +/-- The position the second slot queries, as a string. -/ +noncomputable def conP' (y : List Bool) : List Bool := + posAt (f (pair (conX y) (conRho' r y))) (conC4 y) + +/-- The answer recorded in the first slot. -/ +def conB (y : List Bool) : List Bool := + wBlock (conW y) (conC1 y * Q + conC3 y) 1 + +/-- The answer recorded in the second slot. -/ +def conB' (y : List Bool) : List Bool := + wBlock (conW y) (conC2 y * Q + conC4 y) 1 + +/-- The verdict of one iteration: when both slots are real and query the same +position, their answers must agree. -/ +noncomputable def conChk (y : List Bool) : List Bool := + Cobham.selectHead + (andBit (Cobham.eqFlag (conP f r y) (conP' f r y)) + (notBit (emptyFlag (conP f r y)))) + (Cobham.eqFlag (conB Q y) (conB' Q y)) [true] + +/-! ### Polynomial time -/ + +variable (hf : f ∈ FP) + (hr : (fun x : List Bool => List.replicate (r x.length) true) ∈ FP) + +include hr in +theorem conRho_mem_FP : conRho r ∈ FP := by + have ht : (fun y : List Bool => List.replicate (r (conX y).length) true) ∈ FP := by + have := mem_FP_comp conX_mem_FP hr + simpa using this + exact coinStr_mem_FP ht unary_conC1_mem_FP + +include hr in +theorem conRho'_mem_FP : conRho' r ∈ FP := by + have ht : (fun y : List Bool => List.replicate (r (conX y).length) true) ∈ FP := by + have := mem_FP_comp conX_mem_FP hr + simpa using this + exact coinStr_mem_FP ht unary_conC2_mem_FP + +include hf hr in +theorem conP_mem_FP : conP f r ∈ FP := by + have hb : (fun y => f (pair (conX y) (conRho r y))) ∈ FP := by + have := mem_FP_comp (Cobham.pairFn_mem_FP conX_mem_FP (conRho_mem_FP r hr)) hf + simpa using this + have := posAt_mem_FP unary_conC3_mem_FP hb + refine mem_FP_of_eq this fun y => ?_ + rw [conP, List.length_replicate] + +include hf hr in +theorem conP'_mem_FP : conP' f r ∈ FP := by + have hb : (fun y => f (pair (conX y) (conRho' r y))) ∈ FP := by + have := mem_FP_comp (Cobham.pairFn_mem_FP conX_mem_FP (conRho'_mem_FP r hr)) hf + simpa using this + have := posAt_mem_FP unary_conC4_mem_FP hb + refine mem_FP_of_eq this fun y => ?_ + rw [conP', List.length_replicate] + +/-- The offset of a slot in the witness, in unary. -/ +theorem offset_mem_FP {c d : List Bool → ℕ} + (hc : (fun y => List.replicate (c y) true) ∈ FP) + (hd : (fun y => List.replicate (d y) true) ∈ FP) : + (fun y => List.replicate (c y * Q + d y) false) ∈ FP := by + have hQ : (fun _ : List Bool => List.replicate Q false) ∈ FP := + Cobham.const_replicate_mem_FP Q + have hmul : (fun y => List.replicate ((c y) * Q) false) ∈ FP := by + have := Cobham.mulLenFn_mem_FP hc hQ + refine mem_FP_of_eq this fun y => ?_ + rw [List.length_replicate, List.length_replicate] + have hzero : (fun y => List.replicate (d y) false) ∈ FP := by + have := zeroBlockFn_mem_FP hd + refine mem_FP_of_eq this fun y => ?_ + rw [List.length_replicate] + have := Cobham.appendFn_mem_FP hmul hzero + refine mem_FP_of_eq this fun y => ?_ + rw [← List.replicate_add] + +theorem conB_mem_FP : conB Q ∈ FP := by + have hs := offset_mem_FP Q unary_conC1_mem_FP unary_conC3_mem_FP + have hl : (fun _ : List Bool => [false]) ∈ FP := constFn_mem_FP [false] + have := wBlock_mem_FP conW_mem_FP hs hl + refine mem_FP_of_eq this fun y => ?_ + rw [conB, List.length_replicate] + rfl + +theorem conB'_mem_FP : conB' Q ∈ FP := by + have hs := offset_mem_FP Q unary_conC2_mem_FP unary_conC4_mem_FP + have hl : (fun _ : List Bool => [false]) ∈ FP := constFn_mem_FP [false] + have := wBlock_mem_FP conW_mem_FP hs hl + refine mem_FP_of_eq this fun y => ?_ + rw [conB', List.length_replicate] + rfl + +include hf hr in +theorem conChk_mem_FP : conChk f r Q ∈ FP := by + refine Cobham.selectHeadFn_mem_FP ?_ ?_ (constFn_mem_FP [true]) + · exact andBitFn_mem_FP (eqFlagFn_mem_FP (conP_mem_FP f r hf hr) (conP'_mem_FP f r hf hr)) + (notBitFn_mem_FP (emptyFlagFn_mem_FP (conP_mem_FP f r hf hr))) + · exact eqFlagFn_mem_FP (conB_mem_FP Q) (conB'_mem_FP Q) + +/-- One iteration of the consistency check. -/ +noncomputable def consInner : Language := {y | ∃ b ∈ conChk f r Q y, b = true} + +include hf hr in +theorem consInner_mem_P : consInner f r Q ∈ P := + mem_P_of_decisionFn (conChk_mem_FP f r Q hf hr) fun _ => Iff.rfl + +/-- The two inner loops, over the pair of query indices. -/ +noncomputable def consL3 : Language := + {y | ∀ i' < Q, pair y (List.replicate i' true) ∈ consInner f r Q} + +/-- The outer of the two query-index loops. -/ +noncomputable def consL2 : Language := + {y | ∀ i < Q, pair y (List.replicate i true) ∈ consL3 f r Q} + +/-- The outer loop over the second coin string. -/ +noncomputable def consL1 : Language := + {y | ∀ c' < 2 ^ r (pairFst (pairFst y)).length, + pair y (List.replicate c' true) ∈ consL2 f r Q} + +/-- **The consistency check**, as a language of `pair x w`. -/ +noncomputable def consLang : Language := + {z | ∀ c < 2 ^ r (pairFst z).length, + pair z (List.replicate c true) ∈ consL1 f r Q} + +open scoped Complexity in +include hf hr in +theorem consLang_mem_P (hrlog : r =O fun n => Nat.log 2 n) : consLang f r Q ∈ P := by + have hQ : (fun _ : List Bool => List.replicate Q true) ∈ FP := + constFn_mem_FP (List.replicate Q true) + have hQ' : (fun z : List Bool => List.replicate Q true) ∈ FP := hQ + have h3 : consL3 f r Q ∈ P := + forall_unary_mem_P (consInner_mem_P f r Q hf hr) hQ + have h2 : consL2 f r Q ∈ P := forall_unary_mem_P h3 hQ' + have hexp : (fun z : List Bool => + List.replicate (2 ^ r (pairFst z).length) true) ∈ FP := by + have := mem_FP_comp Cobham.fstBlock_mem_FP (unaryExp_mem_FP_of_bigO_log hr hrlog) + simpa using this + have hexp2 : (fun y : List Bool => + List.replicate (2 ^ r (pairFst (pairFst y)).length) true) ∈ FP := by + have := mem_FP_comp Cobham.fstBlock_mem_FP hexp + simpa using this + have h1 : consL1 f r Q ∈ P := forall_unary_mem_P h2 hexp2 + exact forall_unary_mem_P h1 hexp + +/-! ### What one iteration says -/ + +theorem exists_eqFlag_iff (a b : List Bool) : + (∃ z ∈ Cobham.eqFlag a b, z = true) ↔ a = b := by + rcases Cobham.eqFlag_flag a b with h | h + · rw [h] + simp only [List.mem_singleton, exists_eq_left] + exact ⟨fun _ => (Cobham.eqFlag_eq_true_iff a b).mp h, fun _ => trivial⟩ + · rw [h] + simp only [List.mem_singleton, exists_eq_left, false_iff, Bool.false_eq_true] + intro hab + rw [(Cobham.eqFlag_eq_true_iff a b).mpr hab] at h + simp at h + +theorem mem_consInner_iff (y : List Bool) : + y ∈ consInner f r Q + ↔ (conP f r y = conP' f r y ∧ conP f r y ≠ [] → conB Q y = conB' Q y) := by + rw [consInner, Set.mem_setOf_eq, conChk] + by_cases hcase : conP f r y = conP' f r y ∧ conP f r y ≠ [] + · obtain ⟨heq, hne⟩ := hcase + have h1 : Cobham.eqFlag (conP f r y) (conP' f r y) = [true] := + (Cobham.eqFlag_eq_true_iff _ _).mpr heq + have h2 : emptyFlag (conP f r y) = [false] := by + cases hp : conP f r y with + | nil => exact absurd hp hne + | cons b t => rw [emptyFlag_cons] + rw [h1, h2] + simp only [notBit, andBit, caseBit₀_cons, cond_false, cond_true] + rw [selectHead_cons_true, exists_eqFlag_iff] + exact ⟨fun h _ => h, fun h => h ⟨heq, hne⟩⟩ + · have hflag : andBit (Cobham.eqFlag (conP f r y) (conP' f r y)) + (notBit (emptyFlag (conP f r y))) = [false] := by + by_cases heq : conP f r y = conP' f r y + · have hne : conP f r y = [] := by + by_contra hne + exact hcase ⟨heq, hne⟩ + rw [hne, emptyFlag_nil] + simp only [notBit, andBit, caseBit₀_cons] + rcases Cobham.eqFlag_flag ([] : List Bool) (conP' f r y) with h | h <;> + rw [h] <;> simp + · have h1 : Cobham.eqFlag (conP f r y) (conP' f r y) = [false] := by + rcases Cobham.eqFlag_flag (conP f r y) (conP' f r y) with h | h + · exact absurd ((Cobham.eqFlag_eq_true_iff _ _).mp h) heq + · exact h + rw [h1] + simp [andBit] + rw [hflag, selectHead_cons_false] + simp only [List.mem_singleton, exists_eq_left] + exact ⟨fun _ h => absurd h hcase, fun _ => trivial⟩ + + +/-- The packed input of one iteration. -/ +def conArg (x w : List Bool) (c c' i i' : ℕ) : List Bool := + pair (pair (pair (pair (pair x w) (List.replicate c true)) (List.replicate c' true)) + (List.replicate i true)) (List.replicate i' true) + +@[simp] theorem conX_arg (x w : List Bool) (c c' i i' : ℕ) : + conX (conArg x w c c' i i') = x := by + rw [conArg, conX, conY0, conY1, conY2] + simp only [pairFst_pair] + +@[simp] theorem conW_arg (x w : List Bool) (c c' i i' : ℕ) : + conW (conArg x w c c' i i') = w := by + rw [conArg, conW, conY0, conY1, conY2] + simp only [pairFst_pair, pairSnd_pair] + +@[simp] theorem conC1_arg (x w : List Bool) (c c' i i' : ℕ) : + conC1 (conArg x w c c' i i') = c := by + rw [conArg, conC1, conY1, conY2] + simp only [pairFst_pair, pairSnd_pair, List.length_replicate] + +@[simp] theorem conC2_arg (x w : List Bool) (c c' i i' : ℕ) : + conC2 (conArg x w c c' i i') = c' := by + rw [conArg, conC2, conY2] + simp only [pairFst_pair, pairSnd_pair, List.length_replicate] + +@[simp] theorem conC3_arg (x w : List Bool) (c c' i i' : ℕ) : + conC3 (conArg x w c c' i i') = i := by + rw [conArg, conC3] + simp only [pairFst_pair, pairSnd_pair, List.length_replicate] + +@[simp] theorem conC4_arg (x w : List Bool) (c c' i i' : ℕ) : + conC4 (conArg x w c c' i i') = i' := by + rw [conArg, conC4] + simp only [pairSnd_pair, List.length_replicate] + +/-! ### The pieces on a packed argument -/ + +variable (hfspec : ∀ x rr : List Bool, + f (pair x rr) = DataEncode.bitstringEncode (V.positions x rr)) + +theorem conRho_arg {x w : List Bool} {c c' i i' : ℕ} (hc : c < 2 ^ r x.length) : + conRho r (conArg x w c c' i i') + = BitString.toList (PCPVerifier.coinOfIndex (t := r x.length) ⟨c, hc⟩) := by + rw [conRho, conX_arg, conC1_arg, coinStr_eq hc, toList_coinOfIndex] + +theorem conRho'_arg {x w : List Bool} {c c' i i' : ℕ} (hc' : c' < 2 ^ r x.length) : + conRho' r (conArg x w c c' i i') + = BitString.toList (PCPVerifier.coinOfIndex (t := r x.length) ⟨c', hc'⟩) := by + rw [conRho', conX_arg, conC2_arg, coinStr_eq hc', toList_coinOfIndex] + +include hfspec in +theorem conP_arg {x w : List Bool} {c c' i i' : ℕ} (hc : c < 2 ^ r x.length) : + conP f r (conArg x w c c' i i') + = posAt (DataEncode.bitstringEncode + (V.positions x (BitString.toList + (PCPVerifier.coinOfIndex (t := r x.length) ⟨c, hc⟩)))) i := by + rw [conP, conX_arg, conC3_arg, conRho_arg r hc, hfspec] + +include hfspec in +theorem conP'_arg {x w : List Bool} {c c' i i' : ℕ} (hc' : c' < 2 ^ r x.length) : + conP' f r (conArg x w c c' i i') + = posAt (DataEncode.bitstringEncode + (V.positions x (BitString.toList + (PCPVerifier.coinOfIndex (t := r x.length) ⟨c', hc'⟩)))) i' := by + rw [conP', conX_arg, conC4_arg, conRho'_arg r hc', hfspec] + +theorem conB_arg {x w : List Bool} {c c' i i' : ℕ} (hk : c * Q + i < w.length) : + conB Q (conArg x w c c' i i') = [w.getD (c * Q + i) false] := by + rw [conB, conW_arg, conC1_arg, conC3_arg, wBlock_one hk] + +theorem conB'_arg {x w : List Bool} {c c' i i' : ℕ} (hk : c' * Q + i' < w.length) : + conB' Q (conArg x w c c' i i') = [w.getD (c' * Q + i') false] := by + rw [conB', conW_arg, conC2_arg, conC4_arg, wBlock_one hk] + +/-! ### Unfolding the loops -/ + +theorem mem_consLang_iff_forall (x w : List Bool) : + pair x w ∈ consLang f r Q + ↔ ∀ c < 2 ^ r x.length, ∀ c' < 2 ^ r x.length, ∀ i < Q, ∀ i' < Q, + conArg x w c c' i i' ∈ consInner f r Q := by + rw [consLang, Set.mem_setOf_eq, pairFst_pair] + refine forall_congr' fun c => forall_congr' fun _ => ?_ + rw [consL1, Set.mem_setOf_eq, pairFst_pair, pairFst_pair] + refine forall_congr' fun c' => forall_congr' fun _ => ?_ + rw [consL2, Set.mem_setOf_eq] + refine forall_congr' fun i => forall_congr' fun _ => ?_ + rw [consL3, Set.mem_setOf_eq] + rfl + +/-! ### The check is consistency -/ + +include hfspec in +/-- **The consistency check says exactly what it should.** -/ +theorem mem_consLang_iff {x w : List Bool} + (hw : w.length = 2 ^ r x.length * Q) + (hQ : ∀ rr : List Bool, (V.positions x rr).length ≤ Q) : + pair x w ∈ consLang f r Q + ↔ V.Consistent (r x.length) x (V.tableOf (r x.length) Q x w) := by + have hfit : ∀ c i : ℕ, c < 2 ^ r x.length → i < Q → c * Q + i < w.length := by + intro c i hc hi + rw [hw] + calc c * Q + i < c * Q + Q := by omega + _ = (c + 1) * Q := by ring + _ ≤ 2 ^ r x.length * Q := Nat.mul_le_mul_right _ hc + rw [mem_consLang_iff_forall] + constructor + · intro hR ρ ρ' i i' p hp hp' + have hc : PCPVerifier.coinIndex ρ < 2 ^ r x.length := PCPVerifier.coinIndex_lt ρ + have hc' : PCPVerifier.coinIndex ρ' < 2 ^ r x.length := PCPVerifier.coinIndex_lt ρ' + have hi : i < (V.positions x (BitString.toList ρ)).length := by + by_contra hcon + rw [List.getElem?_eq_none (by omega)] at hp + exact absurd hp (by simp) + have hi' : i' < (V.positions x (BitString.toList ρ')).length := by + by_contra hcon + rw [List.getElem?_eq_none (by omega)] at hp' + exact absurd hp' (by simp) + have hiQ : i < Q := lt_of_lt_of_le hi (hQ _) + have hiQ' : i' < Q := lt_of_lt_of_le hi' (hQ _) + have hpi : (V.positions x (BitString.toList ρ))[i]'hi = p := by + rw [List.getElem?_eq_getElem hi] at hp + exact Option.some.inj hp + have hpi' : (V.positions x (BitString.toList ρ'))[i']'hi' = p := by + rw [List.getElem?_eq_getElem hi'] at hp' + exact Option.some.inj hp' + have hstep := hR _ hc _ hc' i hiQ i' hiQ' + rw [mem_consInner_iff, conP_arg V f r hfspec hc, conP'_arg V f r hfspec hc', + PCPVerifier.coinOfIndex_coinIndex ρ hc, + PCPVerifier.coinOfIndex_coinIndex ρ' hc'] at hstep + have hbits := hstep ⟨by rw [posAt_eq_of_lt hi, posAt_eq_of_lt hi', hpi, hpi'], + posAt_ne_nil hi⟩ + rw [conB_arg Q (hfit _ _ hc hiQ), conB'_arg Q (hfit _ _ hc' hiQ')] at hbits + rw [getElem?_tableOf V _ _ _ _ _ hi, getElem?_tableOf V _ _ _ _ _ hi'] + have : w.getD (PCPVerifier.coinIndex ρ * Q + i) false + = w.getD (PCPVerifier.coinIndex ρ' * Q + i') false := by + simpa using hbits + rw [this] + · intro hC c hc c' hc' i hi i' hi' + rw [mem_consInner_iff, conP_arg V f r hfspec hc, conP'_arg V f r hfspec hc', + conB_arg Q (hfit _ _ hc hi), conB'_arg Q (hfit _ _ hc' hi')] + rintro ⟨heq, hne⟩ + set ρ := PCPVerifier.coinOfIndex (t := r x.length) ⟨c, hc⟩ with hρ + set ρ' := PCPVerifier.coinOfIndex (t := r x.length) ⟨c', hc'⟩ with hρ' + have hin : i < (V.positions x (BitString.toList ρ)).length := by + by_contra hcon + exact hne (posAt_eq_nil (by omega)) + have hin' : i' < (V.positions x (BitString.toList ρ')).length := by + by_contra hcon + rw [posAt_eq_nil (l := V.positions x (BitString.toList ρ')) (by omega)] at heq + exact hne heq + have hpe : (V.positions x (BitString.toList ρ))[i]'hin + = (V.positions x (BitString.toList ρ'))[i']'hin' := + (posAt_eq_iff hin hin').mp heq + have hcons := hC ρ ρ' i i' ((V.positions x (BitString.toList ρ))[i]'hin) + (by rw [List.getElem?_eq_getElem hin]) (by rw [List.getElem?_eq_getElem hin', hpe]) + rw [getElem?_tableOf V _ _ _ _ _ hin, getElem?_tableOf V _ _ _ _ _ hin', + coinIndex_coinOfIndex, coinIndex_coinOfIndex] at hcons + simpa using Option.some.inj hcons + +end Consistency + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/ConstraintGraph.lean b/Complexitylib/Classes/PCP/Internal/ConstraintGraph.lean new file mode 100644 index 00000000..d95218b7 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/ConstraintGraph.lean @@ -0,0 +1,223 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Mathlib.Data.Fintype.Pi +public import Mathlib.Data.Finset.Lattice.Fold +public import Mathlib.Data.Rat.Lemmas +public import Mathlib.Tactic.Positivity +public import Mathlib.Tactic.FieldSimp + +/-! +# Constraint graphs and their unsatisfiability value + +The combinatorial core of Dinur's proof of the PCP theorem. A *constraint +graph* over an alphabet `α` is a finite multigraph whose edges each carry a +binary constraint on the labels of their endpoints; an *assignment* labels the +vertices, and the *unsatisfiability value* `unsatVal` is the least fraction of +edges any assignment leaves unsatisfied. + +Dinur's amplification step is a transformation of constraint graphs that +preserves satisfiability (`unsatVal = 0`) while doubling `unsatVal` otherwise, +so all of that argument is phrased in terms of the definitions here. + +## Design + +- Vertices are `Fin numVerts` and edges are *indexed* by `Fin numEdges`, so + parallel edges and self-loops are allowed. Both are essential: powering a + constraint graph produces many parallel walk-edges, and the degree-reduction + and expanderization steps add edges to a graph that may already have them. +- Edges are directed (`tail`/`head`) and constraints are `Bool`-valued + functions of the two endpoint labels, which keeps everything decidable and + computable. Undirected graphs are modelled by including both orientations, + which the analysis of random walks will require explicitly rather than + implicitly. +- `unsatFrac` divides by `numEdges`, so an edgeless graph gets value `0` by + Lean's `x / 0 = 0` convention. Every lemma below is stated so that this is + the mathematically correct answer. + +## Main definitions + +- `ConstraintGraph`, `ConstraintGraph.Assignment`, `ConstraintGraph.Satisfies` +- `ConstraintGraph.unsatFrac` — the fraction of edges an assignment fails +- `ConstraintGraph.unsatVal` — the minimum of `unsatFrac` over all assignments +- `ConstraintGraph.Satisfiable` + +## Main results + +- `unsatFrac_eq_zero_iff` — an assignment wastes no edges exactly when it + satisfies them all +- `exists_assignment_unsatFrac_eq_unsatVal` — the minimum is attained +- `unsatVal_eq_zero_iff_satisfiable` — the gap-`0` case is satisfiability +- `unsatVal_nonneg`, `unsatVal_le_one` +-/ + +@[expose] public section + +namespace Complexity + +/-- A constraint graph over the alphabet `α`: a finite multigraph on the +vertices `Fin numVerts`, with edges indexed by `Fin numEdges`, each edge +carrying a binary constraint on the labels of its endpoints. -/ +structure ConstraintGraph (α : Type) where + /-- The number of vertices; the vertices are `Fin numVerts`. -/ + numVerts : ℕ + /-- The number of edges; the edges are indexed by `Fin numEdges`, so parallel + edges and self-loops are allowed. -/ + numEdges : ℕ + /-- The source of an edge. -/ + tail : Fin numEdges → Fin numVerts + /-- The target of an edge. -/ + head : Fin numEdges → Fin numVerts + /-- The constraint carried by an edge, as a predicate on the labels of its + tail and its head, in that order. -/ + rel : Fin numEdges → α → α → Bool + +namespace ConstraintGraph + +variable {α : Type} {G : ConstraintGraph α} + +/-- An assignment labels every vertex with a symbol of the alphabet. -/ +abbrev Assignment (G : ConstraintGraph α) : Type := Fin G.numVerts → α + +/-- Whether the assignment `a` satisfies the edge `e`, as a `Bool`. -/ +def satisfies (G : ConstraintGraph α) (a : G.Assignment) (e : Fin G.numEdges) : Bool := + G.rel e (a (G.tail e)) (a (G.head e)) + +/-- The assignment `a` satisfies the edge `e`. -/ +def Satisfies (G : ConstraintGraph α) (a : G.Assignment) (e : Fin G.numEdges) : Prop := + G.satisfies a e = true + +instance (G : ConstraintGraph α) (a : G.Assignment) (e : Fin G.numEdges) : + Decidable (G.Satisfies a e) := + inferInstanceAs (Decidable (G.satisfies a e = true)) + +theorem satisfies_iff {a : G.Assignment} {e : Fin G.numEdges} : + G.Satisfies a e ↔ G.rel e (a (G.tail e)) (a (G.head e)) = true := Iff.rfl + +/-- The edges left unsatisfied by `a`. -/ +def unsatEdges (G : ConstraintGraph α) (a : G.Assignment) : Finset (Fin G.numEdges) := + Finset.univ.filter fun e => ¬ G.Satisfies a e + +@[simp] theorem mem_unsatEdges {a : G.Assignment} {e : Fin G.numEdges} : + e ∈ G.unsatEdges a ↔ ¬ G.Satisfies a e := by + simp [unsatEdges] + +/-- The fraction of edges that `a` leaves unsatisfied. An edgeless graph has +value `0`. -/ +def unsatFrac (G : ConstraintGraph α) (a : G.Assignment) : ℚ := + ((G.unsatEdges a).card : ℚ) / (G.numEdges : ℚ) + +theorem unsatFrac_nonneg (a : G.Assignment) : 0 ≤ G.unsatFrac a := by + unfold unsatFrac; positivity + +theorem card_unsatEdges_le (a : G.Assignment) : (G.unsatEdges a).card ≤ G.numEdges := by + simpa using Finset.card_le_univ (G.unsatEdges a) + +theorem unsatFrac_le_one (a : G.Assignment) : G.unsatFrac a ≤ 1 := by + rcases Nat.eq_zero_or_pos G.numEdges with h | h + · simp [unsatFrac, h] + · have hpos : (0 : ℚ) < (G.numEdges : ℚ) := by exact_mod_cast h + rw [unsatFrac, div_le_one hpos] + exact_mod_cast card_unsatEdges_le a + +/-- An assignment wastes no edges exactly when it satisfies every edge. This +holds for the edgeless graph too, where both sides are trivially true. -/ +theorem unsatFrac_eq_zero_iff {a : G.Assignment} : + G.unsatFrac a = 0 ↔ ∀ e, G.Satisfies a e := by + constructor + · intro h e + by_contra he + have hne : (G.unsatEdges a).Nonempty := ⟨e, by simpa using he⟩ + have hcard : 0 < (G.unsatEdges a).card := Finset.card_pos.mpr hne + have hm : 0 < G.numEdges := lt_of_lt_of_le hcard (card_unsatEdges_le a) + have hmq : (0 : ℚ) < (G.numEdges : ℚ) := by exact_mod_cast hm + have hcq : (0 : ℚ) < ((G.unsatEdges a).card : ℚ) := by exact_mod_cast hcard + rw [unsatFrac, div_eq_zero_iff] at h + rcases h with h | h + · exact absurd h (ne_of_gt hcq) + · exact absurd h (ne_of_gt hmq) + · intro h + have : G.unsatEdges a = ∅ := by + ext e; simpa using h e + simp [unsatFrac, this] + +/-- A constraint graph is satisfiable when some assignment satisfies every +edge. -/ +def Satisfiable (G : ConstraintGraph α) : Prop := ∃ a : G.Assignment, ∀ e, G.Satisfies a e + +section Value + +variable [Fintype α] [Nonempty α] + +/-- The unsatisfiability value: the least fraction of edges any assignment +leaves unsatisfied. -/ +noncomputable def unsatVal (G : ConstraintGraph α) : ℚ := + (Finset.univ : Finset G.Assignment).inf' Finset.univ_nonempty G.unsatFrac + +theorem unsatVal_le (a : G.Assignment) : G.unsatVal ≤ G.unsatFrac a := + Finset.inf'_le _ (Finset.mem_univ a) + +theorem le_unsatVal {c : ℚ} (h : ∀ a : G.Assignment, c ≤ G.unsatFrac a) : c ≤ G.unsatVal := + Finset.le_inf' _ _ fun a _ => h a + +/-- The minimum defining `unsatVal` is attained. -/ +theorem exists_assignment_unsatFrac_eq_unsatVal (G : ConstraintGraph α) : + ∃ a : G.Assignment, G.unsatFrac a = G.unsatVal := by + obtain ⟨a, -, ha⟩ := Finset.exists_mem_eq_inf' (Finset.univ_nonempty) G.unsatFrac + exact ⟨a, ha.symm⟩ + +theorem unsatVal_nonneg (G : ConstraintGraph α) : 0 ≤ G.unsatVal := + le_unsatVal fun a => unsatFrac_nonneg a + +theorem unsatVal_le_one (G : ConstraintGraph α) : G.unsatVal ≤ 1 := by + obtain ⟨a, ha⟩ := G.exists_assignment_unsatFrac_eq_unsatVal + exact ha ▸ unsatFrac_le_one a + +/-- The zero-gap case is exactly satisfiability. -/ +theorem unsatVal_eq_zero_iff_satisfiable (G : ConstraintGraph α) : + G.unsatVal = 0 ↔ G.Satisfiable := by + constructor + · intro h + obtain ⟨a, ha⟩ := G.exists_assignment_unsatFrac_eq_unsatVal + exact ⟨a, unsatFrac_eq_zero_iff.mp (ha.trans h)⟩ + · rintro ⟨a, ha⟩ + have h0 : G.unsatFrac a = 0 := unsatFrac_eq_zero_iff.mpr ha + exact le_antisymm (h0 ▸ unsatVal_le a) G.unsatVal_nonneg + +omit [Fintype α] in +/-- An unsatisfiable graph has an edge, since otherwise any labelling works. -/ +theorem numEdges_pos_of_not_satisfiable (h : ¬ G.Satisfiable) : 0 < G.numEdges := by + rcases Nat.eq_zero_or_pos G.numEdges with h0 | h0 + · exact absurd ⟨(fun _ => Classical.arbitrary α : G.Assignment), fun e => absurd e.isLt + (by simp [h0])⟩ h + · exact h0 + +omit [Fintype α] in +/-- On an unsatisfiable graph every assignment fails at least one edge, so its +value is at least one edge's worth. -/ +theorem inv_numEdges_le_unsatFrac (h : ¬ G.Satisfiable) (a : G.Assignment) : + 1 / (G.numEdges : ℚ) ≤ G.unsatFrac a := by + have hpos : 0 < G.numEdges := numEdges_pos_of_not_satisfiable h + have hne : (G.unsatEdges a).Nonempty := by + by_contra hcon + rw [Finset.not_nonempty_iff_eq_empty] at hcon + refine h ⟨a, fun e => ?_⟩ + by_contra he + simpa [hcon] using (mem_unsatEdges (a := a) (e := e)).mpr he + have hcard : (1 : ℚ) ≤ ((G.unsatEdges a).card : ℚ) := by + exact_mod_cast Finset.card_pos.mpr hne + rw [unsatFrac] + gcongr + +theorem inv_numEdges_le_unsatVal (h : ¬ G.Satisfiable) : + 1 / (G.numEdges : ℚ) ≤ G.unsatVal := + le_unsatVal fun a => inv_numEdges_le_unsatFrac h a + +end Value + +end ConstraintGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/CubeBlocks.lean b/Complexitylib/Classes/PCP/Internal/CubeBlocks.lean new file mode 100644 index 00000000..87e33542 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/CubeBlocks.lean @@ -0,0 +1,323 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.BooleanAnalysis.FourierExpansion +public import Mathlib.Logic.Equiv.Fin.Basic + +/-! +# Blocks of a cube, and marginals + +An assignment tester makes several reads, each with its own randomness. The +Fourier layer supplies probability over *one* cube point (`Pr`) and over a pair +(`Pr₂`), but not over `k` independent points, and building a `k`-fold product +measure would be a detour. + +The alternative taken here is to bundle: the tester's whole random string is a +single point of a larger cube, and each read uses its own block. What makes this +work is that the uniform measure on the big cube restricts to the uniform measure +on each block — proved below — so the existing one-variable lemmas apply to each +read, and the existing union bound combines them. + +## Main definitions + +- `Complexity.leftBlock`, `Complexity.rightBlock` — the two halves of a point + +## Main results + +- `Complexity.prob_leftBlock`, `Complexity.prob_rightBlock` — a block is + uniformly distributed +- `Complexity.prob_leftBlock_rightBlock` — marginals compose, so any fixed + number of blocks can be bundled +- `Complexity.prob₂_eq_prob_blocks` — a probability over a pair is a + probability over one bundled point +- `Complexity.prob_le_of_imp_of_good` — transferring an observed acceptance + probability along a likely good event +- `Complexity.prob_blocks`, `Complexity.prob_blocks_ge` — conditioning on an + earlier block, so a read point may depend on earlier randomness +-/ + +@[expose] public section + +namespace Complexity + +open BooleanAnalysis + +variable {a b : ℕ} + +/-- The first block of a point of the combined cube. -/ +def leftBlock (z : Cube (a + b)) : Cube a := fun i => z (Fin.castAdd b i) + +/-- The second block of a point of the combined cube. -/ +def rightBlock (z : Cube (a + b)) : Cube b := fun j => z (Fin.natAdd a j) + +theorem leftBlock_append (x : Cube a) (y : Cube b) : + leftBlock (Fin.append x y) = x := by + funext i + show Fin.append x y (Fin.castAdd b i) = x i + rw [Fin.append_left] + +theorem rightBlock_append (x : Cube a) (y : Cube b) : + rightBlock (Fin.append x y) = y := by + funext j + show Fin.append x y (Fin.natAdd a j) = y j + rw [Fin.append_right] + +/-- Splitting a point of the combined cube into its blocks is a bijection. -/ +noncomputable def cubeBlockEquiv (a b : ℕ) : Cube a × Cube b ≃ Cube (a + b) := + Fin.appendEquiv a b + +theorem leftBlock_blockEquiv (p : Cube a × Cube b) : + leftBlock (cubeBlockEquiv a b p) = p.1 := leftBlock_append p.1 p.2 + +theorem rightBlock_blockEquiv (p : Cube a × Cube b) : + rightBlock (cubeBlockEquiv a b p) = p.2 := rightBlock_append p.1 p.2 + +/-- **The first block is uniform.** A predicate depending only on the first +block has the same probability over the combined cube as over its own. -/ +theorem prob_leftBlock (P : Cube a → Prop) : + Pr[fun z : Cube (a + b) => P (leftBlock z)] = Pr[P] := by + classical + rw [BooleanAnalysis.prob, BooleanAnalysis.prob, expect_unfold, expect_unfold] + have hsum : ∑ z : Cube (a + b), indicator (fun z => P (leftBlock z)) z + = ∑ p : Cube a × Cube b, indicator P p.1 := by + refine Fintype.sum_equiv (cubeBlockEquiv a b).symm _ _ fun z => ?_ + show indicator (fun z => P (leftBlock z)) z + = indicator P ((cubeBlockEquiv a b).symm z).1 + have hz : leftBlock z = ((cubeBlockEquiv a b).symm z).1 := by + rw [← leftBlock_blockEquiv ((cubeBlockEquiv a b).symm z), Equiv.apply_symm_apply] + simp only [indicator, hz] + rw [hsum, Fintype.sum_prod_type] + have hinner : ∀ x : Cube a, ∑ _y : Cube b, indicator P x + = (2 : ℝ) ^ b * indicator P x := by + intro x + rw [Finset.sum_const, Finset.card_univ, nsmul_eq_mul] + have hcard : Fintype.card (Cube b) = 2 ^ b := by + show Fintype.card (Fin b → ZMod 2) = 2 ^ b + rw [Fintype.card_fun, ZMod.card, Fintype.card_fin] + rw [hcard] + norm_num + rw [Finset.sum_congr rfl fun x _ => hinner x, ← Finset.mul_sum] + rw [pow_add] + field_simp + +/-- **The second block is uniform** too. -/ +theorem prob_rightBlock (P : Cube b → Prop) : + Pr[fun z : Cube (a + b) => P (rightBlock z)] = Pr[P] := by + classical + rw [BooleanAnalysis.prob, BooleanAnalysis.prob, expect_unfold, expect_unfold] + have hsum : ∑ z : Cube (a + b), indicator (fun z => P (rightBlock z)) z + = ∑ p : Cube a × Cube b, indicator P p.2 := by + refine Fintype.sum_equiv (cubeBlockEquiv a b).symm _ _ fun z => ?_ + show indicator (fun z => P (rightBlock z)) z + = indicator P ((cubeBlockEquiv a b).symm z).2 + have hz : rightBlock z = ((cubeBlockEquiv a b).symm z).2 := by + rw [← rightBlock_blockEquiv ((cubeBlockEquiv a b).symm z), Equiv.apply_symm_apply] + simp only [indicator, hz] + rw [hsum, Fintype.sum_prod_type_right] + have hinner : ∀ y : Cube b, ∑ _x : Cube a, indicator P y + = (2 : ℝ) ^ a * indicator P y := by + intro y + rw [Finset.sum_const, Finset.card_univ, nsmul_eq_mul] + have hcard : Fintype.card (Cube a) = 2 ^ a := by + show Fintype.card (Fin a → ZMod 2) = 2 ^ a + rw [Fintype.card_fun, ZMod.card, Fintype.card_fin] + rw [hcard] + norm_num + rw [Finset.sum_congr rfl fun y _ => hinner y, ← Finset.mul_sum] + rw [pow_add] + field_simp + +/-! ### A union bound over many reads -/ + +theorem prob_of_forall {m : ℕ} {P : Cube m → Prop} (h : ∀ x, P x) : Pr[P] = 1 := by + classical + have hc := BooleanAnalysis.Internal.prob_compl P + have hzero : Pr[fun x => ¬ P x] = 0 := by + rw [BooleanAnalysis.prob, expect_unfold] + have hterm : ∀ x : Cube m, indicator (fun x => ¬ P x) x = 0 := by + intro x + simp [indicator, h x] + rw [Finset.sum_congr rfl fun x _ => hterm x] + simp + linarith + +/-! ### Composed blocks -/ + +/-- Marginals compose: a block of a block is still uniform. Iterating this gives +a bundled random string with any fixed number of independent blocks, which is all +a tester with constantly many reads needs. -/ +theorem prob_leftBlock_rightBlock {a b c : ℕ} (P : Cube b → Prop) : + Pr[fun z : Cube (a + (b + c)) => P (leftBlock (rightBlock z))] = Pr[P] := by + have h1 : Pr[fun z : Cube (a + (b + c)) => P (leftBlock (rightBlock z))] + = Pr[fun w : Cube (b + c) => P (leftBlock w)] := + prob_rightBlock (fun w : Cube (b + c) => P (leftBlock w)) + rw [h1] + exact prob_leftBlock P + +theorem prob_rightBlock_rightBlock {a b c : ℕ} (P : Cube c → Prop) : + Pr[fun z : Cube (a + (b + c)) => P (rightBlock (rightBlock z))] = Pr[P] := by + have h1 : Pr[fun z : Cube (a + (b + c)) => P (rightBlock (rightBlock z))] + = Pr[fun w : Cube (b + c) => P (rightBlock w)] := + prob_rightBlock (fun w : Cube (b + c) => P (rightBlock w)) + rw [h1] + exact prob_rightBlock P + +/-! ### Pairs are two blocks -/ + +/-- **`Pr₂` is a bundled `Pr`.** The Fourier layer's probability over a pair of +independent points is the probability over a single point of the doubled cube, +read as two blocks. + +This is the bridge that lets the pair-based statements (the consistency check) +and the block-based statements (the self-corrected reads) be combined: after +rewriting, both are probabilities over one cube, so a union bound applies. -/ +theorem prob₂_eq_prob_blocks {n : ℕ} (P : Cube n → Cube n → Prop) : + Pr₂[P] = Pr[fun z : Cube (n + n) => P (leftBlock z) (rightBlock z)] := by + classical + have hL : Pr₂[P] + = 1 / 2 ^ n * ∑ x : Cube n, (1 / 2 ^ n * ∑ y : Cube n, indicator (P x) y) := by + rw [BooleanAnalysis.prob₂, expect_unfold] + exact congrArg _ (Finset.sum_congr rfl fun x _ => expect_unfold _) + have hR : Pr[fun z : Cube (n + n) => P (leftBlock z) (rightBlock z)] + = 1 / 2 ^ (n + n) * ∑ p : Cube n × Cube n, indicator (P p.1) p.2 := by + rw [BooleanAnalysis.prob, expect_unfold] + refine congrArg _ (Fintype.sum_equiv (cubeBlockEquiv n n).symm _ _ fun z => ?_) + show indicator (fun z => P (leftBlock z) (rightBlock z)) z + = indicator (P ((cubeBlockEquiv n n).symm z).1) ((cubeBlockEquiv n n).symm z).2 + have h1 : leftBlock z = ((cubeBlockEquiv n n).symm z).1 := by + rw [← leftBlock_blockEquiv ((cubeBlockEquiv n n).symm z), Equiv.apply_symm_apply] + have h2 : rightBlock z = ((cubeBlockEquiv n n).symm z).2 := by + rw [← rightBlock_blockEquiv ((cubeBlockEquiv n n).symm z), Equiv.apply_symm_apply] + simp only [indicator, h1, h2] + rw [hL, hR, Fintype.sum_prod_type, ← Finset.mul_sum, pow_add] + field_simp + +/-- A pair drawn from the first block of a bundled string is a uniform pair, +so a `Pr₂` statement can be read as a statement about the bundled randomness +that the tester's other reads also draw from. -/ +theorem prob_pair_block {n c : ℕ} (P : Cube n → Cube n → Prop) : + Pr[fun z : Cube ((n + n) + c) => + P (leftBlock (leftBlock z)) (rightBlock (leftBlock z))] = Pr₂[P] := by + have h := prob_leftBlock (b := c) (fun w : Cube (n + n) => P (leftBlock w) (rightBlock w)) + rw [h, prob₂_eq_prob_blocks] + +/-- **Transfer along a likely good event.** If `E` implies `F` whenever `A` +holds, then `E` is no likelier than `F` plus the chance that `A` fails. + +This is how a tester's observed acceptance is converted into a statement about +the decoded tables: `E` is "the tester accepts", `A` is "every self-corrected +read returned the true value", and `F` is the check as made on the decoded +codewords. -/ +theorem prob_le_of_imp_of_good {m : ℕ} {E F A : Cube m → Prop} + (h : ∀ z, E z → A z → F z) : Pr[E] ≤ Pr[F] + (1 - Pr[A]) := by + classical + have hub : Pr[fun z => ¬ ¬ E z] ≤ Pr[F] + Pr[fun z => ¬ A z] := + BooleanAnalysis.Internal.prob_union_bound (P := fun z => ¬ E z) fun z hz => by + have hE : E z := not_not.mp hz + by_cases hA : A z + · exact Or.inl (h z hE hA) + · exact Or.inr hA + have hfun : (fun z => ¬ ¬ E z) = E := by funext z; simp + rw [hfun] at hub + have hA := BooleanAnalysis.Internal.prob_compl A + linarith + +/-! ### Conditioning on an earlier block -/ + +/-- **Fubini for blocks.** A predicate reading both blocks has probability equal +to the average, over the first block, of its conditional probability in the +second. + +This is what lets a tester choose *where* to read using early randomness and +still get a uniform correction string: the read point is fixed by `u`, and the +inner probability is the ordinary one-variable statement. -/ +theorem prob_blocks {a b : ℕ} (Q : Cube a → Cube b → Prop) : + Pr[fun z : Cube (a + b) => Q (leftBlock z) (rightBlock z)] + = 𝔼[fun u : Cube a => Pr[Q u]] := by + classical + have hL : Pr[fun z : Cube (a + b) => Q (leftBlock z) (rightBlock z)] + = 1 / 2 ^ (a + b) * ∑ p : Cube a × Cube b, indicator (Q p.1) p.2 := by + rw [BooleanAnalysis.prob, expect_unfold] + refine congrArg _ (Fintype.sum_equiv (cubeBlockEquiv a b).symm _ _ fun z => ?_) + show indicator (fun z => Q (leftBlock z) (rightBlock z)) z + = indicator (Q ((cubeBlockEquiv a b).symm z).1) ((cubeBlockEquiv a b).symm z).2 + have h1 : leftBlock z = ((cubeBlockEquiv a b).symm z).1 := by + rw [← leftBlock_blockEquiv ((cubeBlockEquiv a b).symm z), Equiv.apply_symm_apply] + have h2 : rightBlock z = ((cubeBlockEquiv a b).symm z).2 := by + rw [← rightBlock_blockEquiv ((cubeBlockEquiv a b).symm z), Equiv.apply_symm_apply] + simp only [indicator, h1, h2] + have hR : 𝔼[fun u : Cube a => Pr[Q u]] + = 1 / 2 ^ a * ∑ u : Cube a, (1 / 2 ^ b * ∑ v : Cube b, indicator (Q u) v) := by + rw [expect_unfold] + exact congrArg _ (Finset.sum_congr rfl fun u _ => expect_unfold _) + rw [hL, hR, Fintype.sum_prod_type, ← Finset.mul_sum, pow_add] + field_simp + +/-- **A bound that holds for every earlier outcome holds overall.** -/ +theorem prob_blocks_ge {a b : ℕ} (Q : Cube a → Cube b → Prop) (c : ℝ) + (h : ∀ u, c ≤ Pr[Q u]) : + c ≤ Pr[fun z : Cube (a + b) => Q (leftBlock z) (rightBlock z)] := by + classical + rw [prob_blocks, expect_unfold] + have hcard : (Finset.univ : Finset (Cube a)).card = 2 ^ a := by + rw [Finset.card_univ] + show Fintype.card (Fin a → ZMod 2) = 2 ^ a + rw [Fintype.card_fun, ZMod.card, Fintype.card_fin] + have hsum : ∑ _u : Cube a, c ≤ ∑ u : Cube a, Pr[Q u] := + Finset.sum_le_sum fun u _ => h u + rw [Finset.sum_const, hcard, nsmul_eq_mul] at hsum + push_cast at hsum + have hpa : (0 : ℝ) < 2 ^ a := by positivity + rw [one_div, inv_mul_eq_div, le_div_iff₀ hpa, mul_comm] + exact hsum + +/-- **Two likely events are jointly likely.** The union bound in the form the +tester uses: each read fails with its own probability, and the failures add. -/ +theorem prob_and_ge {m : ℕ} {P Q : Cube m → Prop} {p q : ℝ} + (hP : 1 - p ≤ Pr[P]) (hQ : 1 - q ≤ Pr[Q]) : + 1 - (p + q) ≤ Pr[fun z => P z ∧ Q z] := by + classical + have hcP := BooleanAnalysis.Internal.prob_compl P + have hcQ := BooleanAnalysis.Internal.prob_compl Q + have hcPQ := BooleanAnalysis.Internal.prob_compl fun z => P z ∧ Q z + have hunion := BooleanAnalysis.Internal.prob_union_bound + (P := fun z => P z ∧ Q z) (Q := fun z => ¬ P z) (R := fun z => ¬ Q z) + fun z hz => by + by_cases hp : P z + · exact Or.inr fun hq => hz ⟨hp, hq⟩ + · exact Or.inl hp + linarith + +/-- **A likely event happens.** Used to turn a probabilistic guarantee about a +tester's reads into a single random string on which every read is right — the +argument for a check whose conclusion is deterministic. -/ +theorem exists_of_prob_pos {m : ℕ} {P : Cube m → Prop} (h : 0 < Pr[P]) : ∃ z, P z := by + classical + by_contra hcon + push Not at hcon + have hzero : Pr[P] = 0 := by + rw [BooleanAnalysis.prob, expect_unfold] + have hind : ∀ z : Cube m, indicator P z = 0 := fun z => by + simp [indicator, hcon z] + rw [Finset.sum_congr rfl fun z _ => hind z] + simp + linarith + +/-- A probability is a count over the cube. -/ +theorem prob_eq_card_div {m : ℕ} (P : Cube m → Prop) [DecidablePred P] : + Pr[P] = ((Finset.univ.filter P).card : ℝ) / 2 ^ m := by + classical + rw [BooleanAnalysis.prob, expect_unfold] + have h : ∑ z : Cube m, indicator P z = ((Finset.univ.filter P).card : ℝ) := by + rw [Finset.card_filter] + push_cast + exact Finset.sum_congr rfl fun z _ => by + simp only [indicator] + split_ifs <;> rfl + rw [h] + ring + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/DartCorrelation.lean b/Complexitylib/Classes/PCP/Internal/DartCorrelation.lean new file mode 100644 index 00000000..a7144f17 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/DartCorrelation.lean @@ -0,0 +1,538 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.WalkDart +public import Complexitylib.Classes.PCP.Internal.Mixing +public import Mathlib.Algebra.BigOperators.Intervals +public import Mathlib.Algebra.Ring.GeomSum + +/-! +# Counting a dart set by tail and by head + +The bookkeeping that turns `WalkDart.sum_two_darts_fixed` — the operator form of +"the walk crosses `F` at step `k` and again at step `l`" — into something +`Mixing.mixing_sq` can estimate. + +Two counts of a dart set are needed. `dartCount F v` counts the `F`-darts +*leaving* `v`, and `headCount F z` counts those *arriving* at `z`. Both sum to +`F.card`, and each is at most the degree. The first is the function whose +`stepIter` the second crossing sees; the second is the measure the first +crossing leaves behind. + +## Main definitions + +- `RegGraph.dartCount`, `RegGraph.headCount` + +## Main results + +- `RegGraph.sum_dartCount`, `RegGraph.sum_headCount` — both count `F` +- `RegGraph.dartCount_le`, `RegGraph.headCount_le` — at most the degree +- `RegGraph.sum_indicator_nbr` — a sum over `F`-darts of a function of the head + is a sum over vertices weighted by `headCount` +- `RegGraph.step_sum`, `RegGraph.stepIter_sum` — the walk operator is linear + over finite sums +- `RegGraph.sum_indicator_dartCount`, `RegGraph.sum_indicator_mul` — indicator + sums collapse to the two counts +- `RegGraph.sum_two_crossings` — two crossings of `F`, in operator form +- `RegGraph.sum_headCount_stepIter_le` — the correlation bound: two crossings + `t` steps apart are independent up to `lam ^ t * deg * |F|` +- `geom_sum_le_inv`, `sum_pairs_geom_le` — summing that over all position pairs + costs only `m / (1 - lam)` +- `RegGraph.sum_pairs_le` — the second moment: all pairs of crossings together +- `card_sq_eq_add_two_mul_pairs` — a squared count is the count plus twice the + ordered pairs +- `card_filter_eq_sum_prod`, `sum_pairs_eq_sum_Ico` — index bookkeeping for the + second moment +-/ + +@[expose] public section + +namespace Complexity + +/-! ### Geometric sums -/ + +/-- A truncated geometric series is bounded by its limit. -/ +theorem geom_sum_le_inv {lam : ℝ} (h0 : 0 ≤ lam) (h1 : lam < 1) (n : ℕ) : + ∑ i ∈ Finset.range n, lam ^ i ≤ 1 / (1 - lam) := by + have hpos : (0 : ℝ) < 1 - lam := by linarith + have hmul : (∑ i ∈ Finset.range n, lam ^ i) * (lam - 1) = lam ^ n - 1 := geom_sum_mul lam n + have hmul' : (∑ i ∈ Finset.range n, lam ^ i) * (1 - lam) = 1 - lam ^ n := by + nlinarith [hmul] + have hpow : (0 : ℝ) ≤ lam ^ n := by positivity + rw [le_div_iff₀ hpos] + linarith [hmul'] + +/-- Summed over all pairs of positions, the separation weights `lam ^ (l-k-1)` +contribute at most `m / (1 - lam)`. -/ +theorem sum_pairs_geom_le {lam : ℝ} (h0 : 0 ≤ lam) (h1 : lam < 1) (m : ℕ) : + ∑ k ∈ Finset.range m, ∑ l ∈ Finset.Ico (k + 1) m, lam ^ (l - k - 1) + ≤ (m : ℝ) * (1 / (1 - lam)) := by + have hinner : ∀ k ∈ Finset.range m, + ∑ l ∈ Finset.Ico (k + 1) m, lam ^ (l - k - 1) ≤ 1 / (1 - lam) := by + intro k _ + have hre : ∑ l ∈ Finset.Ico (k + 1) m, lam ^ (l - k - 1) + = ∑ i ∈ Finset.range (m - (k + 1)), lam ^ i := by + rw [Finset.sum_Ico_eq_sum_range] + refine Finset.sum_congr rfl fun i _ => ?_ + congr 1 + omega + rw [hre] + exact geom_sum_le_inv h0 h1 _ + calc ∑ k ∈ Finset.range m, ∑ l ∈ Finset.Ico (k + 1) m, lam ^ (l - k - 1) + ≤ ∑ _k ∈ Finset.range m, (1 / (1 - lam)) := Finset.sum_le_sum hinner + _ = (m : ℝ) * (1 / (1 - lam)) := by + rw [Finset.sum_const, Finset.card_range, nsmul_eq_mul] + +/-! ### Squares and ordered pairs -/ + +/-- The square of a count is the count plus twice the ordered pairs. This is how +`∑ N ^ 2` in the second-moment method becomes a sum over *pairs* of crossings, +which is what the correlation bound estimates. -/ +theorem card_sq_eq_add_two_mul_pairs {ι : Type*} [LinearOrder ι] [DecidableEq ι] + (S : Finset ι) : + S.card ^ 2 = S.card + 2 * ((S ×ˢ S).filter fun p => p.1 < p.2).card := by + classical + have hswap : ((S ×ˢ S).filter fun p => p.1 < p.2).card + = ((S ×ˢ S).filter fun p => p.2 < p.1).card := by + refine Finset.card_bij (fun p _ => (p.2, p.1)) ?_ ?_ ?_ + · intro p hp + simp only [Finset.mem_filter, Finset.mem_product] at hp ⊢ + exact ⟨⟨hp.1.2, hp.1.1⟩, hp.2⟩ + · intro p _ p' _ hpp + have h1 : p.2 = p'.2 := congrArg Prod.fst hpp + have h2 : p.1 = p'.1 := congrArg Prod.snd hpp + exact Prod.ext h2 h1 + · intro p hp + simp only [Finset.mem_filter, Finset.mem_product] at hp + refine ⟨(p.2, p.1), ?_, rfl⟩ + simp only [Finset.mem_filter, Finset.mem_product] + exact ⟨⟨hp.1.2, hp.1.1⟩, hp.2⟩ + have hdiag : ((S ×ˢ S).filter fun p => p.1 = p.2).card = S.card := by + refine Finset.card_bij (fun p _ => p.1) ?_ ?_ ?_ + · intro p hp + simp only [Finset.mem_filter, Finset.mem_product] at hp + exact hp.1.1 + · intro p hp p' hp' hpp + simp only [Finset.mem_filter, Finset.mem_product] at hp hp' + refine Prod.ext hpp ?_ + rw [← hp.2, ← hp'.2] + exact hpp + · intro a ha + refine ⟨(a, a), ?_, rfl⟩ + simp [ha] + have hnotlt : ((S ×ˢ S).filter fun p => ¬ p.1 < p.2).card + = ((S ×ˢ S).filter fun p => p.1 = p.2).card + + ((S ×ˢ S).filter fun p => p.2 < p.1).card := by + rw [← Finset.card_union_of_disjoint] + · congr 1 + ext p + simp only [Finset.mem_filter, Finset.mem_union, Finset.mem_product] + constructor + · rintro ⟨hmem, hlt⟩ + rcases lt_trichotomy p.1 p.2 with h | h | h + · exact absurd h hlt + · exact Or.inl ⟨hmem, h⟩ + · exact Or.inr ⟨hmem, h⟩ + · rintro (⟨hmem, heq⟩ | ⟨hmem, hgt⟩) + · exact ⟨hmem, by rw [heq]; exact lt_irrefl _⟩ + · exact ⟨hmem, not_lt_of_gt hgt⟩ + · refine Finset.disjoint_left.mpr fun p hp hp' => ?_ + simp only [Finset.mem_filter] at hp hp' + rw [hp.2] at hp' + exact absurd hp'.2 (lt_irrefl _) + have htotal : (S ×ˢ S).card + = ((S ×ˢ S).filter fun p => p.1 < p.2).card + + ((S ×ˢ S).filter fun p => ¬ p.1 < p.2).card := + (Finset.card_filter_add_card_filter_not _).symm + rw [Finset.card_product] at htotal + rw [sq] + omega + +/-- A count of a conjunction is a sum of products of indicators. -/ +theorem card_filter_eq_sum_prod {ι : Type*} [Fintype ι] (P Q : ι → Prop) + [DecidablePred P] [DecidablePred Q] : + (((Finset.univ.filter fun i => P i ∧ Q i).card : ℕ) : ℝ) + = ∑ i, (if P i then (1 : ℝ) else 0) * (if Q i then (1 : ℝ) else 0) := by + classical + rw [Finset.card_filter] + push_cast + refine Finset.sum_congr rfl fun i _ => ?_ + by_cases hp : P i <;> by_cases hq : Q i <;> simp [hp, hq] + +/-- A sum over ordered pairs below `T`, as an iterated sum. -/ +theorem sum_pairs_eq_sum_Ico {M : Type*} [AddCommMonoid M] (T : ℕ) (f : ℕ × ℕ → M) : + ∑ p ∈ ((Finset.range T) ×ˢ (Finset.range T)).filter fun p => p.1 < p.2, f p + = ∑ k ∈ Finset.range T, ∑ l ∈ Finset.Ico (k + 1) T, f (k, l) := by + classical + rw [Finset.sum_filter, Finset.sum_product] + refine Finset.sum_congr rfl fun k _ => ?_ + rw [← Finset.sum_filter] + congr 1 + ext l + simp only [Finset.mem_filter, Finset.mem_range, Finset.mem_Ico] + omega + +namespace RegGraph + +variable (G : RegGraph) + +/-! ### Linearity of the walk operator -/ + +theorem step_sum {ι : Type*} (s : Finset ι) (f : ι → G.V → ℝ) (v : G.V) : + G.step (fun w => ∑ i ∈ s, f i w) v = ∑ i ∈ s, G.step (f i) v := by + simp only [step] + rw [← Finset.sum_div] + congr 1 + exact Finset.sum_comm + +theorem stepIter_sum {ι : Type*} (s : Finset ι) (f : ι → G.V → ℝ) (t : ℕ) (v : G.V) : + G.stepIter t (fun w => ∑ i ∈ s, f i w) v = ∑ i ∈ s, G.stepIter t (f i) v := by + induction t generalizing v with + | zero => simp + | succ t ih => + rw [stepIter_succ] + have hfun : G.stepIter t (fun w => ∑ i ∈ s, f i w) + = fun w => ∑ i ∈ s, G.stepIter t (f i) w := by + funext w + exact ih w + rw [hfun, G.step_sum] + exact Finset.sum_congr rfl fun i _ => by rw [← stepIter_succ] + +/-- How many darts of `F` leave `v`. -/ +def dartCount (F : Finset (G.V × G.D)) (v : G.V) : ℕ := + (F.filter fun p => p.1 = v).card + +/-- How many darts of `F` arrive at `z`. -/ +def headCount (F : Finset (G.V × G.D)) (z : G.V) : ℕ := + (F.filter fun p => G.nbr p.1 p.2 = z).card + +theorem sum_dartCount (F : Finset (G.V × G.D)) : + ∑ v : G.V, G.dartCount F v = F.card := + (Finset.card_eq_sum_card_fiberwise fun p _ => Finset.mem_univ p.1).symm + +theorem sum_headCount (F : Finset (G.V × G.D)) : + ∑ z : G.V, G.headCount F z = F.card := + (Finset.card_eq_sum_card_fiberwise fun p _ => Finset.mem_univ (G.nbr p.1 p.2)).symm + +theorem dartCount_le (F : Finset (G.V × G.D)) (v : G.V) : G.dartCount F v ≤ G.deg := by + classical + have hsub : (F.filter fun p => p.1 = v) ⊆ ({v} : Finset G.V) ×ˢ (Finset.univ : Finset G.D) := by + intro p hp + simp only [Finset.mem_filter] at hp + simp only [Finset.mem_product, Finset.mem_singleton, Finset.mem_univ, and_true] + exact hp.2 + calc G.dartCount F v ≤ (({v} : Finset G.V) ×ˢ (Finset.univ : Finset G.D)).card := + Finset.card_le_card hsub + _ = G.deg := by + rw [Finset.card_product, Finset.card_singleton, one_mul, Finset.card_univ] + rfl + +/-- The darts arriving at `z` correspond, under reversal, to the darts leaving +`z`, so there are at most `deg` of them. -/ +theorem headCount_le (F : Finset (G.V × G.D)) (z : G.V) : G.headCount F z ≤ G.deg := by + classical + have hinj : Set.InjOn (fun p : G.V × G.D => (G.rot p).2) + (F.filter fun p => G.nbr p.1 p.2 = z) := by + intro p hp p' hp' hval + simp only [Finset.coe_filter, Set.mem_setOf_eq] at hp hp' + have hp1 : (G.rot p).1 = z := hp.2 + have hp'1 : (G.rot p').1 = z := hp'.2 + have hpair : G.rot p = G.rot p' := Prod.ext (hp1.trans hp'1.symm) hval + have := congrArg G.rot hpair + rwa [G.rot_involutive p, G.rot_involutive p'] at this + calc G.headCount F z + ≤ (Finset.univ : Finset G.D).card := by + rw [headCount] + exact Finset.card_le_card_of_injOn _ (fun p _ => Finset.mem_univ _) hinj + _ = G.deg := by rw [Finset.card_univ]; rfl + +/-- Summing a function of a dart's head over `F` is summing over vertices with +the multiplicity `headCount`. -/ +theorem sum_indicator_nbr (F : Finset (G.V × G.D)) (h : G.V → ℝ) : + ∑ p ∈ F, h (G.nbr p.1 p.2) = ∑ z : G.V, (G.headCount F z : ℝ) * h z := by + classical + rw [← Finset.sum_fiberwise_of_maps_to (fun p (_ : p ∈ F) => Finset.mem_univ (G.nbr p.1 p.2)) + (fun p => h (G.nbr p.1 p.2))] + refine Finset.sum_congr rfl fun z _ => ?_ + have hconst : ∀ p ∈ F.filter fun p => G.nbr p.1 p.2 = z, h (G.nbr p.1 p.2) = h z := by + intro p hp + simp only [Finset.mem_filter] at hp + rw [hp.2] + rw [Finset.sum_congr rfl hconst, Finset.sum_const, nsmul_eq_mul] + rfl + +/-! ### Indicator sums -/ + +/-- Summing the `F`-indicator over the labels at a fixed vertex counts the +`F`-darts there. -/ +theorem sum_indicator_dartCount (F : Finset (G.V × G.D)) (z : G.V) : + ∑ b : G.D, (if (z, b) ∈ F then (1 : ℝ) else 0) = (G.dartCount F z : ℝ) := by + classical + rw [Finset.sum_boole] + congr 1 + rw [dartCount] + refine Finset.card_bij (fun b _ => (z, b)) ?_ ?_ ?_ + · intro b hb + simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hb ⊢ + simpa using hb + · intro b _ b' _ hbb + exact (Prod.ext_iff.mp hbb).2 + · rintro ⟨y, b⟩ hp + simp only [Finset.mem_filter] at hp + obtain ⟨hmem, hy⟩ := hp + subst hy + exact ⟨b, by simpa using hmem, rfl⟩ + +/-- Summing an `F`-weighted function of a dart's head over all darts is summing +it over `F`. -/ +theorem sum_indicator_mul (F : Finset (G.V × G.D)) (φ : G.V → ℝ) : + ∑ a : G.D, ∑ y : G.V, (if (y, a) ∈ F then (1 : ℝ) else 0) * φ (G.nbr y a) + = ∑ p ∈ F, φ (G.nbr p.1 p.2) := by + classical + have hswap : ∑ a : G.D, ∑ y : G.V, (if (y, a) ∈ F then (1 : ℝ) else 0) * φ (G.nbr y a) + = ∑ p : G.V × G.D, (if p ∈ F then (1 : ℝ) else 0) * φ (G.nbr p.1 p.2) := by + rw [Fintype.sum_prod_type] + exact Finset.sum_comm + rw [hswap] + have hite : ∀ p : G.V × G.D, (if p ∈ F then (1 : ℝ) else 0) * φ (G.nbr p.1 p.2) + = if p ∈ F then φ (G.nbr p.1 p.2) else 0 := by + intro p + split <;> simp + rw [Finset.sum_congr rfl fun p _ => hite p, ← Finset.sum_filter] + congr 1 + simp + +/-! ### The two-crossing identity -/ + +/-- **Two crossings, in operator form.** Summed over all starts and all label +sequences, the walks that cross `F` at step `k` and again at step `l` are counted +by the walk operator applied to the dart counts: the first crossing leaves the +measure `headCount`, and the second is seen through `l - k - 1` steps of the +walk. `Mixing.mixing_sq` estimates exactly this expression. -/ +theorem sum_two_crossings (F : Finset (G.V × G.D)) {k l m : ℕ} (hkl : k < l) (hl : l < m) : + (∑ x : G.V, ∑ r : Fin m → G.D, + (if (G.walkAt m x r k, r ⟨k, by omega⟩) ∈ F then (1 : ℝ) else 0) + * (if (G.walkAt m x r l, r ⟨l, by omega⟩) ∈ F then (1 : ℝ) else 0)) + = (G.deg : ℝ) ^ (m - 2) + * ∑ z : G.V, (G.headCount F z : ℝ) + * G.stepIter (l - k - 1) (fun w => (G.dartCount F w : ℝ)) z := by + classical + set h : G.V → G.D → ℝ := fun y a => if (y, a) ∈ F then (1 : ℝ) else 0 with hh + have hx : ∀ x : G.V, (∑ r : Fin m → G.D, h (G.walkAt m x r k) (r ⟨k, by omega⟩) + * h (G.walkAt m x r l) (r ⟨l, by omega⟩)) + = (G.deg : ℝ) ^ (m - 2) * ∑ a : G.D, ∑ b : G.D, + G.stepIter k (fun y => h y a + * G.stepIter (l - k - 1) (fun z => h z b) (G.nbr y a)) x := + fun x => G.sum_two_darts_fixed h h k l m hkl hl x + rw [Finset.sum_congr rfl fun x _ => hx x, ← Finset.mul_sum] + congr 1 + have hswap : ∑ x : G.V, ∑ a : G.D, ∑ b : G.D, + G.stepIter k (fun y => h y a + * G.stepIter (l - k - 1) (fun z => h z b) (G.nbr y a)) x + = ∑ a : G.D, ∑ b : G.D, ∑ x : G.V, + G.stepIter k (fun y => h y a + * G.stepIter (l - k - 1) (fun z => h z b) (G.nbr y a)) x := by + rw [Finset.sum_comm] + exact Finset.sum_congr rfl fun a _ => Finset.sum_comm + rw [hswap] + have hcollapse : ∀ a : G.D, ∀ b : G.D, ∑ x : G.V, + G.stepIter k (fun y => h y a + * G.stepIter (l - k - 1) (fun z => h z b) (G.nbr y a)) x + = ∑ y : G.V, h y a * G.stepIter (l - k - 1) (fun z => h z b) (G.nbr y a) := by + intro a b + exact G.sum_stepIter k _ + rw [Finset.sum_congr rfl fun a _ => Finset.sum_congr rfl fun b _ => hcollapse a b] + have hby : ∀ a : G.D, ∑ b : G.D, ∑ y : G.V, + h y a * G.stepIter (l - k - 1) (fun z => h z b) (G.nbr y a) + = ∑ y : G.V, h y a + * G.stepIter (l - k - 1) (fun w => (G.dartCount F w : ℝ)) (G.nbr y a) := by + intro a + rw [Finset.sum_comm] + refine Finset.sum_congr rfl fun y _ => ?_ + rw [← Finset.mul_sum] + congr 1 + rw [← G.stepIter_sum Finset.univ (fun b z => h z b) (l - k - 1) (G.nbr y a)] + congr 1 + funext z + exact G.sum_indicator_dartCount F z + rw [Finset.sum_congr rfl fun a _ => hby a] + rw [G.sum_indicator_mul F + (fun w => G.stepIter (l - k - 1) (fun w' => (G.dartCount F w' : ℝ)) w)] + exact G.sum_indicator_nbr F _ + +/-! ### Bounding the correlation -/ + +/-- The variance of a dart count is at most `deg * |F|`, since no vertex carries +more than `deg` darts of `F`. -/ +theorem sum_sq_headCount_le (F : Finset (G.V × G.D)) : + ∑ z : G.V, ((G.headCount F z : ℝ)) ^ 2 ≤ (G.deg : ℝ) * (F.card : ℝ) := by + have hterm : ∀ z : G.V, ((G.headCount F z : ℝ)) ^ 2 + ≤ (G.deg : ℝ) * (G.headCount F z : ℝ) := by + intro z + have h := G.headCount_le F z + have hR : (G.headCount F z : ℝ) ≤ (G.deg : ℝ) := by exact_mod_cast h + nlinarith + calc ∑ z : G.V, ((G.headCount F z : ℝ)) ^ 2 + ≤ ∑ z : G.V, (G.deg : ℝ) * (G.headCount F z : ℝ) := Finset.sum_le_sum fun z _ => hterm z + _ = (G.deg : ℝ) * ∑ z : G.V, (G.headCount F z : ℝ) := by rw [Finset.mul_sum] + _ = (G.deg : ℝ) * (F.card : ℝ) := by + congr 1 + rw [← Nat.cast_sum, G.sum_headCount F] + +theorem sum_sq_dartCount_le (F : Finset (G.V × G.D)) : + ∑ v : G.V, ((G.dartCount F v : ℝ)) ^ 2 ≤ (G.deg : ℝ) * (F.card : ℝ) := by + have hterm : ∀ v : G.V, ((G.dartCount F v : ℝ)) ^ 2 + ≤ (G.deg : ℝ) * (G.dartCount F v : ℝ) := by + intro v + have h := G.dartCount_le F v + have hR : (G.dartCount F v : ℝ) ≤ (G.deg : ℝ) := by exact_mod_cast h + nlinarith + calc ∑ v : G.V, ((G.dartCount F v : ℝ)) ^ 2 + ≤ ∑ v : G.V, (G.deg : ℝ) * (G.dartCount F v : ℝ) := Finset.sum_le_sum fun v _ => hterm v + _ = (G.deg : ℝ) * ∑ v : G.V, (G.dartCount F v : ℝ) := by rw [Finset.mul_sum] + _ = (G.deg : ℝ) * (F.card : ℝ) := by + congr 1 + rw [← Nat.cast_sum, G.sum_dartCount F] + +/-- **The correlation bound.** On a graph with spectral bound `lam`, two +crossings of `F` separated by `t` steps are almost independent: the operator +expression exceeds the independent value `|F|² / n` by at most +`lam ^ t * deg * |F|`. -/ +theorem sum_headCount_stepIter_le (F : Finset (G.V × G.D)) {lam : ℝ} + (hlam : 0 ≤ lam) (hspec : G.SpectralBound lam) (hn : 0 < G.order) (t : ℕ) : + ∑ z : G.V, (G.headCount F z : ℝ) + * G.stepIter t (fun w => (G.dartCount F w : ℝ)) z + ≤ (F.card : ℝ) * (F.card : ℝ) / (G.order : ℝ) + + lam ^ t * ((G.deg : ℝ) * (F.card : ℝ)) := by + have hmix := G.mixing_sq hspec hn t (fun z => (G.headCount F z : ℝ)) + (fun w => (G.dartCount F w : ℝ)) + have hsumf : ∑ z : G.V, (G.headCount F z : ℝ) = (F.card : ℝ) := by + rw [← Nat.cast_sum, G.sum_headCount F] + have hsumg : ∑ v : G.V, (G.dartCount F v : ℝ) = (F.card : ℝ) := by + rw [← Nat.cast_sum, G.sum_dartCount F] + rw [hsumf, hsumg] at hmix + set X : ℝ := ∑ z : G.V, (G.headCount F z : ℝ) + * G.stepIter t (fun w => (G.dartCount F w : ℝ)) z with hX + set c : ℝ := (F.card : ℝ) * (F.card : ℝ) / (G.order : ℝ) with hc + set D : ℝ := lam ^ t * ((G.deg : ℝ) * (F.card : ℝ)) with hD + have hDnn : 0 ≤ D := by + rw [hD] + have : (0 : ℝ) ≤ lam ^ t := by positivity + positivity + have hbound : (X - c) ^ 2 ≤ D ^ 2 := by + refine le_trans hmix ?_ + have hvf : (∑ z : G.V, (G.headCount F z : ℝ) ^ 2) - (F.card : ℝ) ^ 2 / (G.order : ℝ) + ≤ (G.deg : ℝ) * (F.card : ℝ) := by + have h1 := G.sum_sq_headCount_le F + have h2 : (0 : ℝ) ≤ (F.card : ℝ) ^ 2 / (G.order : ℝ) := by positivity + linarith + have hvg : (∑ v : G.V, (G.dartCount F v : ℝ) ^ 2) - (F.card : ℝ) ^ 2 / (G.order : ℝ) + ≤ (G.deg : ℝ) * (F.card : ℝ) := by + have h1 := G.sum_sq_dartCount_le F + have h2 : (0 : ℝ) ≤ (F.card : ℝ) ^ 2 / (G.order : ℝ) := by positivity + linarith + have hvfnn : (0 : ℝ) ≤ (∑ z : G.V, (G.headCount F z : ℝ) ^ 2) + - (F.card : ℝ) ^ 2 / (G.order : ℝ) := by + have := G.sum_sq_center_nonneg (fun z => (G.headCount F z : ℝ)) + rwa [G.sum_sq_center hn, hsumf] at this + have hvgnn : (0 : ℝ) ≤ (∑ v : G.V, (G.dartCount F v : ℝ) ^ 2) + - (F.card : ℝ) ^ 2 / (G.order : ℝ) := by + have := G.sum_sq_center_nonneg (fun w => (G.dartCount F w : ℝ)) + rwa [G.sum_sq_center hn, hsumg] at this + have hpow : (0 : ℝ) ≤ lam ^ (2 * t) := by positivity + have hprod : lam ^ (2 * t) + * ((∑ z : G.V, (G.headCount F z : ℝ) ^ 2) - (F.card : ℝ) ^ 2 / (G.order : ℝ)) + * ((∑ v : G.V, (G.dartCount F v : ℝ) ^ 2) - (F.card : ℝ) ^ 2 / (G.order : ℝ)) + ≤ lam ^ (2 * t) * ((G.deg : ℝ) * (F.card : ℝ)) * ((G.deg : ℝ) * (F.card : ℝ)) := by + have hstep : lam ^ (2 * t) + * ((∑ z : G.V, (G.headCount F z : ℝ) ^ 2) - (F.card : ℝ) ^ 2 / (G.order : ℝ)) + ≤ lam ^ (2 * t) * ((G.deg : ℝ) * (F.card : ℝ)) := + mul_le_mul_of_nonneg_left hvf hpow + have hnn2 : (0 : ℝ) ≤ lam ^ (2 * t) + * ((∑ z : G.V, (G.headCount F z : ℝ) ^ 2) + - (F.card : ℝ) ^ 2 / (G.order : ℝ)) := by positivity + nlinarith [hvgnn, hvg] + refine le_trans hprod ?_ + rw [hD] + have h2t : lam ^ (2 * t) = (lam ^ t) ^ 2 := by + rw [← pow_mul, Nat.mul_comm] + rw [h2t] + ring_nf + exact le_refl _ + have habs : X - c ≤ D := by + nlinarith [hbound, hDnn] + linarith + +/-- **The second moment.** Summed over every pair of positions, the two-crossing +correlations exceed the independent value by at most `m / (1 - lam)` times +`deg * |F|`. The counts are supplied abstractly as `C`, since writing the +concrete one inside a sum would need the position bounds pointwise. -/ +theorem sum_pairs_le (F : Finset (G.V × G.D)) {lam : ℝ} (hlam0 : 0 ≤ lam) + (hlam1 : lam < 1) (hspec : G.SpectralBound lam) (hn : 0 < G.order) (m : ℕ) + (C : ℕ → ℕ → ℝ) + (hC : ∀ k ∈ Finset.range m, ∀ l ∈ Finset.Ico (k + 1) m, + C k l = ∑ z : G.V, (G.headCount F z : ℝ) + * G.stepIter (l - k - 1) (fun w => (G.dartCount F w : ℝ)) z) : + ∑ k ∈ Finset.range m, ∑ l ∈ Finset.Ico (k + 1) m, C k l + ≤ (m : ℝ) * (m : ℝ) * ((F.card : ℝ) * (F.card : ℝ) / (G.order : ℝ)) + + (m : ℝ) * (1 / (1 - lam)) * ((G.deg : ℝ) * (F.card : ℝ)) := by + have hbound : ∀ k ∈ Finset.range m, ∀ l ∈ Finset.Ico (k + 1) m, + C k l ≤ (F.card : ℝ) * (F.card : ℝ) / (G.order : ℝ) + + lam ^ (l - k - 1) * ((G.deg : ℝ) * (F.card : ℝ)) := by + intro k hk l hl + rw [hC k hk l hl] + exact G.sum_headCount_stepIter_le F hlam0 hspec hn _ + have hstep1 : ∑ k ∈ Finset.range m, ∑ l ∈ Finset.Ico (k + 1) m, C k l + ≤ ∑ k ∈ Finset.range m, ∑ l ∈ Finset.Ico (k + 1) m, + ((F.card : ℝ) * (F.card : ℝ) / (G.order : ℝ) + + lam ^ (l - k - 1) * ((G.deg : ℝ) * (F.card : ℝ))) := + Finset.sum_le_sum fun k hk => Finset.sum_le_sum fun l hl => hbound k hk l hl + have hsplit : ∑ k ∈ Finset.range m, ∑ l ∈ Finset.Ico (k + 1) m, + ((F.card : ℝ) * (F.card : ℝ) / (G.order : ℝ) + + lam ^ (l - k - 1) * ((G.deg : ℝ) * (F.card : ℝ))) + = (∑ k ∈ Finset.range m, ∑ l ∈ Finset.Ico (k + 1) m, + (F.card : ℝ) * (F.card : ℝ) / (G.order : ℝ)) + + (∑ k ∈ Finset.range m, ∑ l ∈ Finset.Ico (k + 1) m, + lam ^ (l - k - 1) * ((G.deg : ℝ) * (F.card : ℝ))) := by + rw [← Finset.sum_add_distrib] + exact Finset.sum_congr rfl fun k _ => Finset.sum_add_distrib + have hconst : ∑ k ∈ Finset.range m, ∑ l ∈ Finset.Ico (k + 1) m, + (F.card : ℝ) * (F.card : ℝ) / (G.order : ℝ) + ≤ (m : ℝ) * (m : ℝ) * ((F.card : ℝ) * (F.card : ℝ) / (G.order : ℝ)) := by + have hnn : (0 : ℝ) ≤ (F.card : ℝ) * (F.card : ℝ) / (G.order : ℝ) := by positivity + have hinner : ∀ k ∈ Finset.range m, ∑ l ∈ Finset.Ico (k + 1) m, + (F.card : ℝ) * (F.card : ℝ) / (G.order : ℝ) + ≤ (m : ℝ) * ((F.card : ℝ) * (F.card : ℝ) / (G.order : ℝ)) := by + intro k _ + rw [Finset.sum_const, nsmul_eq_mul, Nat.card_Ico] + have hcard : ((m - (k + 1) : ℕ) : ℝ) ≤ (m : ℝ) := by + have : (m - (k + 1) : ℕ) ≤ m := by omega + exact_mod_cast this + exact mul_le_mul_of_nonneg_right hcard hnn + calc ∑ k ∈ Finset.range m, ∑ l ∈ Finset.Ico (k + 1) m, + (F.card : ℝ) * (F.card : ℝ) / (G.order : ℝ) + ≤ ∑ _k ∈ Finset.range m, (m : ℝ) * ((F.card : ℝ) * (F.card : ℝ) / (G.order : ℝ)) := + Finset.sum_le_sum hinner + _ = (m : ℝ) * (m : ℝ) * ((F.card : ℝ) * (F.card : ℝ) / (G.order : ℝ)) := by + rw [Finset.sum_const, Finset.card_range, nsmul_eq_mul] + ring + have hgeom : ∑ k ∈ Finset.range m, ∑ l ∈ Finset.Ico (k + 1) m, + lam ^ (l - k - 1) * ((G.deg : ℝ) * (F.card : ℝ)) + ≤ (m : ℝ) * (1 / (1 - lam)) * ((G.deg : ℝ) * (F.card : ℝ)) := by + have hfactor : ∑ k ∈ Finset.range m, ∑ l ∈ Finset.Ico (k + 1) m, + lam ^ (l - k - 1) * ((G.deg : ℝ) * (F.card : ℝ)) + = (∑ k ∈ Finset.range m, ∑ l ∈ Finset.Ico (k + 1) m, lam ^ (l - k - 1)) + * ((G.deg : ℝ) * (F.card : ℝ)) := by + rw [Finset.sum_mul] + exact Finset.sum_congr rfl fun k _ => (Finset.sum_mul _ _ _).symm + rw [hfactor] + have hnn : (0 : ℝ) ≤ (G.deg : ℝ) * (F.card : ℝ) := by positivity + exact mul_le_mul_of_nonneg_right (sum_pairs_geom_le hlam0 hlam1 m) hnn + linarith [hstep1, hsplit.le, hsplit.ge, hconst, hgeom] + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/DataScan.lean b/Complexitylib/Classes/PCP/Internal/DataScan.lean new file mode 100644 index 00000000..0f67cf00 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/DataScan.lean @@ -0,0 +1,351 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P.Cobham.Internal +public import Complexitylib.Classes.P.Cobham.Internal.Reverse +public import Complexitylib.Classes.P.UnaryLength +public import Complexitylib.Classes.P.Cobham.Internal.HeadOps +public import Complexitylib.Encoding.DataEncode +public import Complexitylib.Classes.PCP.Internal.DataScanSpec + +/-! +# Scanning a serialized `Data` value + +`Data.toBits` writes a value as balanced brackets: `false` opens a node, its +children follow in order, and `true` closes it. Reading one child back out of +such a string is a single left-to-right pass keeping a bracket depth and a count +of the children already passed, collecting bits only while inside the child +asked for. + +This module writes that pass in the form `recFoldClamp` accepts, so that +polynomial-time computability comes from the general fold rather than from a +bespoke machine. The fold recurses head-then-tail, so it runs right to left; the +caller therefore hands it the reversed string. + +The state is `pair (unary depth) (pair (unary count) collected)` and the +workspace is the requested index in unary. Every component stays below the +length of the string being scanned, so a linear clamp suffices. + +## Main definitions + +- `Complexity.DataScan.openStep`, `closeStep` — the two fold steps +- `Complexity.DataScan.childOf` — the scan, packaged as one function + +## Main results + +- `Complexity.DataScan.childOf_mem_FP` — the scan is polynomial time +- `Complexity.DataScan.recFoldClamp_eq_pack` — the fold runs the model +- `Complexity.DataScan.child_flatten` — the packaged scan extracts the child +- `Complexity.DataScan.childCount_flatten` — and counts the children +- `Complexity.DataScan.inner_toBits` — the bits between the outer brackets +-/ + +@[expose] public section + +namespace Complexity + +namespace DataScan + +/-! ### Reading the packed fold argument + +`recFoldClamp` hands each step `pair (pair W st) t`, with `W` the workspace, `st` +the state built so far and `t` the unscanned tail. -/ + +/-- The workspace: the index of the child being extracted, in unary. -/ +def wsOf (z : List Bool) : List Bool := pairFst (pairFst z) + +/-- The state carried by the scan. -/ +def stOf (z : List Bool) : List Bool := pairSnd (pairFst z) + +/-- The bracket depth, in unary. -/ +def depthOf (z : List Bool) : List Bool := pairFst (stOf z) + +/-- The number of children already passed, in unary. -/ +def countOf (z : List Bool) : List Bool := pairFst (pairSnd (stOf z)) + +/-- The bits collected so far. -/ +def accOf (z : List Bool) : List Bool := pairSnd (pairSnd (stOf z)) + +/-- Append the current bit, but only while inside the requested child. -/ +def collect (z : List Bool) (b : Bool) : List Bool := + Cobham.selectHead (Cobham.eqFlag (countOf z) (wsOf z)) (accOf z ++ [b]) (accOf z) + +/-! ### The two steps -/ + +/-- An opening bracket: descend one level. -/ +def openStep (z : List Bool) : List Bool := + pair (true :: depthOf z) (pair (countOf z) (collect z false)) + +/-- A closing bracket: rise one level, and if that returns to the top level, +one more child has been passed. -/ +def closeStep (z : List Bool) : List Bool := + pair (dropOne (depthOf z)) + (pair (Cobham.selectHead (emptyFlag (dropOne (depthOf z))) + (true :: countOf z) (countOf z)) + (collect z true)) + +/-- The state a scan starts from. -/ +def initState : List Bool := pair [] (pair [] []) + +/-! ### Polynomial time -/ + +theorem wsOf_mem_FP : wsOf ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + +theorem stOf_mem_FP : stOf ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + +theorem depthOf_mem_FP : depthOf ∈ FP := + mem_FP_comp stOf_mem_FP Cobham.fstBlock_mem_FP + +theorem countOf_mem_FP : countOf ∈ FP := + mem_FP_comp (mem_FP_comp stOf_mem_FP Cobham.sndBlock_mem_FP) Cobham.fstBlock_mem_FP + +theorem accOf_mem_FP : accOf ∈ FP := + mem_FP_comp (mem_FP_comp stOf_mem_FP Cobham.sndBlock_mem_FP) Cobham.sndBlock_mem_FP + +theorem collect_mem_FP (b : Bool) : (fun z => collect z b) ∈ FP := + Cobham.selectHeadFn_mem_FP (eqFlagFn_mem_FP countOf_mem_FP wsOf_mem_FP) + (Cobham.appendFn_mem_FP accOf_mem_FP (constFn_mem_FP [b])) accOf_mem_FP + +theorem openStep_mem_FP : openStep ∈ FP := + Cobham.pairFn_mem_FP (mem_FP_comp depthOf_mem_FP (Cobham.cons_mem_FP true)) + (Cobham.pairFn_mem_FP countOf_mem_FP (collect_mem_FP false)) + +theorem closeStep_mem_FP : closeStep ∈ FP := by + have hdrop : (fun z => dropOne (depthOf z)) ∈ FP := dropOneFn_mem_FP depthOf_mem_FP + refine Cobham.pairFn_mem_FP hdrop (Cobham.pairFn_mem_FP ?_ (collect_mem_FP true)) + exact Cobham.selectHeadFn_mem_FP (emptyFlagFn_mem_FP hdrop) + (mem_FP_comp countOf_mem_FP (Cobham.cons_mem_FP true)) countOf_mem_FP + +/-- **The scan.** On `pair (unary i) s` it runs the two steps over `s`, keeping +every intermediate state within `p.eval` bits, and returns the collected bits. -/ +def childOf (p : Polynomial ℕ) (z : List Bool) : List Bool := + pairSnd (pairSnd + (Cobham.recFoldClamp openStep closeStep (p.eval z.length) initState + (pairFst z) (pairSnd z))) + +theorem childOf_mem_FP (p : Polynomial ℕ) : childOf p ∈ FP := by + have hfold := Cobham.recFoldClamp_mem_FP openStep_mem_FP closeStep_mem_FP + (constFn_mem_FP initState) p + exact mem_FP_comp (mem_FP_comp hfold Cobham.sndBlock_mem_FP) Cobham.sndBlock_mem_FP + +/-! ### The fold runs the model + +The state the fold carries is the model's state written out: two unary counters +and the collected bits. Once that is checked step by step, the fold and the +model agree, provided the clamp is wide enough never to truncate. -/ + +/-- The model's state, written out as a bitstring. -/ +def pack (st : ℕ × ℕ × List Bool) : List Bool := + pair (List.replicate st.1 true) (pair (List.replicate st.2.1 true) st.2.2) + +theorem pack_length (st : ℕ × ℕ × List Bool) : + (pack st).length = 2 * st.1 + 2 * st.2.1 + st.2.2.length + 4 := by + rw [pack, pair_length, pair_length, List.length_replicate, List.length_replicate] + omega + +theorem pack_init : pack (0, 0, []) = initState := rfl + +theorem eqFlag_replicate (c i : ℕ) : + Cobham.eqFlag (List.replicate c true) (List.replicate i true) + = if c = i then [true] else [false] := by + by_cases h : c = i + · rw [if_pos h, h] + exact (Cobham.eqFlag_eq_true_iff _ _).mpr rfl + · rw [if_neg h] + rcases Cobham.eqFlag_flag (List.replicate c true) (List.replicate i true) with hf | hf + · rw [Cobham.eqFlag_eq_true_iff] at hf + exact absurd (by simpa using congrArg List.length hf) h + · exact hf + +theorem depthOf_pack (i : ℕ) (st : ℕ × ℕ × List Bool) (t : List Bool) : + depthOf (pair (pair (List.replicate i true) (pack st)) t) = List.replicate st.1 true := by + rw [depthOf, stOf, pairFst_pair, pairSnd_pair, pack, pairFst_pair] + +theorem countOf_pack (i : ℕ) (st : ℕ × ℕ × List Bool) (t : List Bool) : + countOf (pair (pair (List.replicate i true) (pack st)) t) + = List.replicate st.2.1 true := by + rw [countOf, stOf, pairFst_pair, pairSnd_pair, pack, pairSnd_pair, + pairFst_pair] + +theorem collect_pack (i : ℕ) (st : ℕ × ℕ × List Bool) (t : List Bool) (b : Bool) : + collect (pair (pair (List.replicate i true) (pack st)) t) b + = if st.2.1 = i then st.2.2 ++ [b] else st.2.2 := by + rw [collect, countOf, accOf, wsOf, stOf, pairFst_pair, pairSnd_pair, + pack, pairFst_pair, pairSnd_pair, pairFst_pair, + pairSnd_pair, eqFlag_replicate] + by_cases h : st.2.1 = i <;> simp [h] + +theorem openStep_pack (i : ℕ) (st : ℕ × ℕ × List Bool) (t : List Bool) : + openStep (pair (pair (List.replicate i true) (pack st)) t) + = pack (stepSpec i st false) := by + rw [openStep, depthOf_pack, countOf_pack, collect_pack, stepSpec, pack] + simp [List.replicate_succ] + +theorem closeStep_pack (i : ℕ) (st : ℕ × ℕ × List Bool) (t : List Bool) : + closeStep (pair (pair (List.replicate i true) (pack st)) t) + = pack (stepSpec i st true) := by + have hdrop : dropOne (List.replicate st.1 true) = List.replicate (st.1 - 1) true := by + cases st.1 with + | zero => rfl + | succ n => rw [List.replicate_succ]; rfl + have hflag : emptyFlag (List.replicate (st.1 - 1) true) + = if st.1 - 1 = 0 then [true] else [false] := by + cases h : st.1 - 1 with + | zero => simp + | succ n => rw [List.replicate_succ, emptyFlag_cons]; simp + rw [closeStep, depthOf_pack, countOf_pack, collect_pack, hdrop, hflag, stepSpec, pack] + by_cases h : st.1 - 1 = 0 <;> simp [h, List.replicate_succ] + +/-! ### The state stays small -/ + +theorem runSpec_bounds (i : ℕ) (s : List Bool) : + ∀ (d c : ℕ) (acc : List Bool), + (runSpec i (d, c, acc) s).1 ≤ d + s.length ∧ + (runSpec i (d, c, acc) s).2.1 ≤ c + s.length ∧ + (runSpec i (d, c, acc) s).2.2.length ≤ acc.length + s.length := by + induction s with + | nil => intro d c acc; simp + | cons b s ih => + intro d c acc + rw [runSpec_cons] + have hstep : stepSpec i (d, c, acc) b + = ((stepSpec i (d, c, acc) b).1, (stepSpec i (d, c, acc) b).2.1, + (stepSpec i (d, c, acc) b).2.2) := rfl + have h1 : (stepSpec i (d, c, acc) b).1 ≤ d + 1 := by + cases b <;> simp [stepSpec] + omega + have h2 : (stepSpec i (d, c, acc) b).2.1 ≤ c + 1 := by + cases b <;> simp [stepSpec] + split <;> omega + have h3 : (stepSpec i (d, c, acc) b).2.2.length ≤ acc.length + 1 := by + have hb2 : (stepSpec i (d, c, acc) b).2.2 = if c = i then acc ++ [b] else acc := by + cases b <;> rfl + rw [hb2] + by_cases hc : c = i <;> simp [hc] + rw [hstep] + obtain ⟨j1, j2, j3⟩ := ih (stepSpec i (d, c, acc) b).1 (stepSpec i (d, c, acc) b).2.1 + (stepSpec i (d, c, acc) b).2.2 + refine ⟨?_, ?_, ?_⟩ + · exact le_trans j1 (by simp [List.length_cons]; omega) + · exact le_trans j2 (by simp [List.length_cons]; omega) + · exact le_trans j3 (by simp [List.length_cons]; omega) + +theorem pack_runSpec_length_le (i : ℕ) (s : List Bool) : + (pack (runSpec i (0, 0, []) s)).length ≤ 5 * s.length + 4 := by + obtain ⟨h1, h2, h3⟩ := runSpec_bounds i s 0 0 [] + rw [pack_length] + simp only [List.length_nil, Nat.zero_add] at h1 h2 h3 + omega + +/-- **The fold runs the model.** Reading the reversed string with the clamped +fold gives exactly the model's state, as long as the clamp is wide enough. -/ +theorem recFoldClamp_eq_pack (i bound : ℕ) (l : List Bool) + (hb : 5 * l.length + 4 ≤ bound) : + Cobham.recFoldClamp openStep closeStep bound initState (List.replicate i true) l + = pack (runSpec i (0, 0, []) l.reverse) := by + induction l with + | nil => + rw [Cobham.recFoldClamp, ← pack_init] + simp only [List.reverse_nil, runSpec_nil] + refine List.take_of_length_le ?_ + have := pack_runSpec_length_le i ([] : List Bool) + simp only [runSpec_nil] at this + simp only [List.length_nil, Nat.mul_zero, Nat.zero_add] at hb + exact le_trans (by simpa using pack_length (0, 0, ([] : List Bool)) ▸ le_refl _) hb + | cons b l ih => + have hb' : 5 * l.length + 4 ≤ bound := by + simp only [List.length_cons] at hb + omega + rw [Cobham.recFoldClamp, ih hb'] + have hstep : (bif b then closeStep else openStep) + (pair (pair (List.replicate i true) (pack (runSpec i (0, 0, []) l.reverse))) l) + = pack (stepSpec i (runSpec i (0, 0, []) l.reverse) b) := by + cases b + · exact openStep_pack _ _ _ + · exact closeStep_pack _ _ _ + rw [hstep] + have hval : pack (stepSpec i (runSpec i (0, 0, []) l.reverse) b) + = pack (runSpec i (0, 0, []) (b :: l).reverse) := by + rw [List.reverse_cons, runSpec_append] + rfl + rw [hval] + refine List.take_of_length_le ?_ + exact le_trans (pack_runSpec_length_le i ((b :: l).reverse)) (by simpa using hb) + +/-! ### The packaged scan -/ + +/-- A clamp wide enough for any scan: the state never exceeds `5 n + 4` bits. -/ +noncomputable def scanPoly : Polynomial ℕ := Polynomial.C 5 * Polynomial.X + Polynomial.C 4 + +@[simp] theorem scanPoly_eval (n : ℕ) : scanPoly.eval n = 5 * n + 4 := by + rw [scanPoly] + simp + +/-- The scan's argument: the index in unary paired with the reversed string, the +order the fold consumes. -/ +def scanArg (i : ℕ) (s : List Bool) : List Bool := pair (List.replicate i true) s.reverse + +theorem scanArg_mem_FP {a b : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) : + (fun z => scanArg (a z).length (b z)) ∈ FP := by + have hrep : (fun z => List.replicate (a z).length true) ∈ FP := by + have := mem_FP_comp ha unaryLength_mem_FP + simpa using this + have hrev : (fun z => (b z).reverse) ∈ FP := by + have := mem_FP_comp hb reverse_mem_FP + simpa using this + exact Cobham.pairFn_mem_FP hrep hrev + +/-- **The scan extracts the child.** Reading the concatenated serializations of +`xs` returns the `i`-th one, or nothing when there is no such child. -/ +theorem child_flatten (i : ℕ) (xs : List Data) : + childOf scanPoly (scanArg i ((xs.map Data.toBits).flatten)) + = ((xs[i]?).map Data.toBits).getD [] := by + set F := (xs.map Data.toBits).flatten with hF + have hlen : (scanArg i F).length = 2 * i + 2 + F.length := by + rw [scanArg, pair_length, List.length_replicate, List.length_reverse] + rw [childOf, scanArg, pairFst_pair, pairSnd_pair, ← scanArg, hlen, + scanPoly_eval, recFoldClamp_eq_pack i _ F.reverse (by simp), + List.reverse_reverse, hF, runSpec_inner, pack, pairSnd_pair, + pairSnd_pair] + +/-- **How many children there are**, in unary: the same pass, reading off the +counter instead of the collected bits. -/ +def childCount (p : Polynomial ℕ) (z : List Bool) : List Bool := + pairFst (pairSnd + (Cobham.recFoldClamp openStep closeStep (p.eval z.length) initState + (pairFst z) (pairSnd z))) + +theorem childCount_mem_FP (p : Polynomial ℕ) : childCount p ∈ FP := by + have hfold := Cobham.recFoldClamp_mem_FP openStep_mem_FP closeStep_mem_FP + (constFn_mem_FP initState) p + exact mem_FP_comp (mem_FP_comp hfold Cobham.sndBlock_mem_FP) Cobham.fstBlock_mem_FP + +theorem childCount_flatten (i : ℕ) (xs : List Data) : + childCount scanPoly (scanArg i ((xs.map Data.toBits).flatten)) + = List.replicate xs.length true := by + set F := (xs.map Data.toBits).flatten with hF + have hlen : (scanArg i F).length = 2 * i + 2 + F.length := by + rw [scanArg, pair_length, List.length_replicate, List.length_reverse] + rw [childCount, scanArg, pairFst_pair, pairSnd_pair, ← scanArg, hlen, + scanPoly_eval, recFoldClamp_eq_pack i _ F.reverse (by simp), + List.reverse_reverse, hF, runSpec_inner, pack, pairSnd_pair, + pairFst_pair] + +/-- The bits strictly between the outer brackets of a serialized list. -/ +theorem inner_toBits (xs : List Data) : + ((Data.l xs).toBits.drop 1).take ((Data.l xs).toBits.length - 2) + = (xs.map Data.toBits).flatten := by + have hb : (Data.l xs).toBits + = false :: ((xs.map Data.toBits).flatten ++ [true]) := Data.toBits_l xs + rw [hb] + simp + +end DataScan + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/DataScanSpec.lean b/Complexitylib/Classes/PCP/Internal/DataScanSpec.lean new file mode 100644 index 00000000..12e1aed8 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/DataScanSpec.lean @@ -0,0 +1,207 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Encoding.DataEncode + +/-! +# What the bracket scan computes + +A model of the left-to-right pass over a serialized `Data` value: a bracket +depth, a count of the top-level children already passed, and the bits collected +while inside the child that was asked for. This file is about the model alone — +that it really does extract the requested child — and says nothing about +computability. + +Two facts drive everything. The depth and the count evolve without looking at +the collected bits, and the collected bits only ever grow at the end +(`runSpec_append_acc`); and a serialized value read at any depth returns to that +depth, having contributed exactly its own serialization when it was the child +being sought (`runSpec_toBits`). + +## Main definitions + +- `Complexity.DataScan.runSpec` — the model of the pass + +## Main results + +- `Complexity.DataScan.runSpec_toBits` — reading one serialized value +- `Complexity.DataScan.runSpec_flatten` — reading a whole run of children +- `Complexity.DataScan.runSpec_inner` — the pass over a serialized list returns + the requested child +-/ + +@[expose] public section + +namespace Complexity + +namespace DataScan + +/-- One step of the pass: `false` opens a bracket, `true` closes one, and the +bit joins the output exactly when the count matches the child sought. -/ +def stepSpec (i : ℕ) (st : ℕ × ℕ × List Bool) (b : Bool) : ℕ × ℕ × List Bool := + let acc' := if st.2.1 = i then st.2.2 ++ [b] else st.2.2 + if b then (st.1 - 1, if st.1 - 1 = 0 then st.2.1 + 1 else st.2.1, acc') + else (st.1 + 1, st.2.1, acc') + +/-- The pass over a whole string. -/ +def runSpec (i : ℕ) (st : ℕ × ℕ × List Bool) (s : List Bool) : ℕ × ℕ × List Bool := + s.foldl (stepSpec i) st + +@[simp] theorem runSpec_nil (i : ℕ) (st : ℕ × ℕ × List Bool) : runSpec i st [] = st := rfl + +theorem runSpec_append (i : ℕ) (st : ℕ × ℕ × List Bool) (s t : List Bool) : + runSpec i st (s ++ t) = runSpec i (runSpec i st s) t := by + rw [runSpec, runSpec, runSpec, List.foldl_append] + +theorem runSpec_cons (i : ℕ) (st : ℕ × ℕ × List Bool) (b : Bool) (s : List Bool) : + runSpec i st (b :: s) = runSpec i (stepSpec i st b) s := rfl + +/-- **The collected bits only grow at the end.** A prefix already present in the +output is carried through untouched, and the depth and count do not see it. -/ +theorem runSpec_append_acc (i : ℕ) (s : List Bool) : + ∀ (d c : ℕ) (pre acc : List Bool), + runSpec i (d, c, pre ++ acc) s + = ((runSpec i (d, c, acc) s).1, (runSpec i (d, c, acc) s).2.1, + pre ++ (runSpec i (d, c, acc) s).2.2) := by + induction s with + | nil => intro d c pre acc; rfl + | cons b s ih => + intro d c pre acc + rw [runSpec_cons, runSpec_cons] + have hstep : stepSpec i (d, c, pre ++ acc) b + = ((stepSpec i (d, c, acc) b).1, (stepSpec i (d, c, acc) b).2.1, + pre ++ (stepSpec i (d, c, acc) b).2.2) := by + by_cases hb : b <;> by_cases hc : c = i <;> + simp [stepSpec, hb, hc, List.append_assoc] + rw [hstep] + exact ih _ _ _ _ + +/-- **Reading one serialized value.** Starting at depth `d` with `c` children +already passed, the pass over `y.toBits` returns to depth `d`; it counts one +more child exactly when it was at the top level, and it contributes `y.toBits` +to the output exactly when `y` is the child sought. -/ +theorem runSpec_toBits (i : ℕ) (y : Data) : + ∀ (d c : ℕ) (acc : List Bool), + runSpec i (d, c, acc) y.toBits + = (d, (if d = 0 then c + 1 else c), if c = i then acc ++ y.toBits else acc) := by + induction y using Data.inductionL with + | nil => + intro d c acc + have hnil : (Data.l ([] : List Data)).toBits = [false, true] := by + rw [Data.toBits_l]; simp + rw [hnil] + by_cases hc : c = i <;> + simp [runSpec_cons, stepSpec, hc] + | cons x xs ihx ihxs => + intro d c acc + have hsplit : (Data.l (x :: xs)).toBits + = false :: (x.toBits ++ ((xs.map Data.toBits).flatten ++ [true])) := by + rw [Data.toBits_l] + simp + have htail : (Data.l xs).toBits = false :: ((xs.map Data.toBits).flatten ++ [true]) := by + rw [Data.toBits_l] + set F := (xs.map Data.toBits).flatten ++ [true] with hF + set acc₁ := if c = i then acc ++ [false] else acc with hacc₁ + -- The behaviour of `F` from depth `d + 1`, read off from the tail's statement. + have hxs := ihxs d c acc + rw [htail, runSpec_cons] at hxs + have hstep0 : stepSpec i (d, c, acc) false = (d + 1, c, acc₁) := by + by_cases hc : c = i <;> simp [stepSpec, hc, hacc₁] + rw [hstep0] at hxs + -- Uniformity: read `F` from the empty output instead. + have huni : ∀ a : List Bool, runSpec i (d + 1, c, a) F + = ((runSpec i (d + 1, c, []) F).1, (runSpec i (d + 1, c, []) F).2.1, + a ++ (runSpec i (d + 1, c, []) F).2.2) := by + intro a + have := runSpec_append_acc i F (d + 1) c a [] + simpa using this + have hR := huni acc₁ + rw [hxs] at hR + set R := (runSpec i (d + 1, c, []) F).2.2 with hRdef + have hD : (runSpec i (d + 1, c, []) F).1 = d := by + have := congrArg Prod.fst hR + simpa using this.symm + have hC : (runSpec i (d + 1, c, []) F).2.1 = (if d = 0 then c + 1 else c) := by + have := congrArg (fun p => p.2.1) hR + simpa using this.symm + have hacc : (if c = i then acc ++ (false :: F) else acc) = acc₁ ++ R := by + have := congrArg (fun p => p.2.2) hR + simpa using this + -- Now the value at hand. + rw [hsplit, runSpec_cons, hstep0, runSpec_append, ihx (d + 1) c acc₁, + if_neg (Nat.succ_ne_zero d)] + set acc₂ := if c = i then acc₁ ++ x.toBits else acc₁ with hacc₂ + rw [huni acc₂, hD, hC] + refine Prod.ext rfl (Prod.ext rfl ?_) + by_cases hc : c = i + · have hR' : R = F := by + simp only [hc, hacc₁] at hacc + simpa using hacc.symm + rw [hacc₂, hacc₁, if_pos hc, if_pos hc, if_pos hc, hR'] + simp + · have hR' : R = [] := by + simp only [if_neg hc, hacc₁] at hacc + simpa using hacc.symm + rw [hacc₂, hacc₁, if_neg hc, if_neg hc, if_neg hc, hR'] + simp + +/-! ### A run of children -/ + +/-- The child a pass looking for index `i` picks out of `xs`, having already +passed `c` children. -/ +def selFrom (xs : List Data) (i c : ℕ) : List Bool := + if c ≤ i then ((xs[i - c]?).map Data.toBits).getD [] else [] + +@[simp] theorem selFrom_nil (i c : ℕ) : selFrom [] i c = [] := by + rw [selFrom] + split <;> simp + +/-- **Reading a run of children.** At the top level the pass counts every child +and contributes exactly the one it was asked for. -/ +theorem runSpec_flatten (i : ℕ) : + ∀ (xs : List Data) (c : ℕ) (acc : List Bool), + runSpec i (0, c, acc) ((xs.map Data.toBits).flatten) + = (0, c + xs.length, acc ++ selFrom xs i c) := by + intro xs + induction xs with + | nil => intro c acc; simp + | cons x xs ih => + intro c acc + rw [List.map_cons, List.flatten_cons, runSpec_append, runSpec_toBits, if_pos rfl, ih] + refine Prod.ext rfl (Prod.ext (by simp [List.length_cons]; omega) ?_) + simp only + by_cases hlt : c < i + · have h : c < i := hlt + have h1 : selFrom (x :: xs) i c = selFrom xs i (c + 1) := by + rw [selFrom, selFrom, if_pos (by omega : c ≤ i), if_pos (by omega)] + have : i - c = (i - (c + 1)) + 1 := by omega + rw [this] + simp + rw [if_neg (by omega), h1] + by_cases heq : c = i + · subst heq + have h1 : selFrom (x :: xs) c c = x.toBits := by + rw [selFrom, if_pos (by omega : c ≤ c)] + simp + have h2 : selFrom xs c (c + 1) = [] := by + rw [selFrom, if_neg (by omega)] + rw [if_pos rfl, h1, h2, List.append_assoc, List.append_nil] + · have h : i < c := by omega + have h1 : selFrom (x :: xs) i c = [] := by rw [selFrom, if_neg (by omega)] + have h2 : selFrom xs i (c + 1) = [] := by rw [selFrom, if_neg (by omega)] + rw [if_neg (by omega), h1, h2] + +/-- **The pass over a serialized list.** Reading the bits strictly between the +outer brackets returns the requested child's own serialization. -/ +theorem runSpec_inner (i : ℕ) (xs : List Data) : + runSpec i (0, 0, []) ((xs.map Data.toBits).flatten) + = (0, xs.length, ((xs[i]?).map Data.toBits).getD []) := by + rw [runSpec_flatten, selFrom, if_pos (Nat.zero_le i)] + simp + +end DataScan + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/DegreeReduction.lean b/Complexitylib/Classes/PCP/Internal/DegreeReduction.lean new file mode 100644 index 00000000..439668b1 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/DegreeReduction.lean @@ -0,0 +1,313 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.ConstraintGraph +public import Complexitylib.Classes.PCP.Internal.NumEnc +public import Complexitylib.Classes.PCP.Internal.Expander +public import Complexitylib.Classes.PCP.Internal.RegCSP + +/-! +# Degree reduction + +The first half of Dinur's preprocessing: an arbitrary constraint graph, whose +vertices may have wildly different degrees, is replaced by a *regular* one over +the same alphabet. + +The vertices of the new graph are the **half-edges** of the old one — an edge +together with one of its endpoints — so there are exactly `2 · numEdges` of +them, however lopsided the original degrees were. Each half-edge has + +* one **edge-link**, to the other half of its own edge, carrying that edge's + original constraint, and +* `E.degree` **cloud-links**, wiring together the half-edges that share an + endpoint, by a member of an `ExpanderFamily`, carrying equality constraints. + +So the result is `(1 + E.degree)`-regular by construction, with no padding, and +its size is a constant multiple of the original's — which is what the +amplification bookkeeping needs. + +## The cloud rotation, without dependent types + +A cloud's expander lives on `Fin (cloud size)`, and cloud sizes differ, so the +naive rotation map forces a transport along `owner p' = owner p` that is not +type-correct to rewrite. The fix is `cloudRotAux`, which takes the cloud's +enumeration `l` as a *parameter*: within it the length is fixed, and +involutivity is an ordinary argument about a `Nodup` list. At the top level the +only rewriting needed is `cloudList (owner p') = cloudList (owner p)`, an +equation between plain lists with no dependent type in sight. + +## Main definitions + +- `ConstraintGraph.owner`, `flipHalf`, `cloud`, `cloudList` — the cloud + structure on half-edges +- `ConstraintGraph.cloudRotAux`, `cloudRot` — the cloud-link rotation +- `ConstraintGraph.reduceGraph`, `reduce` — the regular graph and system + +## Main results + +- `ConstraintGraph.cloudRotAux_involutive`, `cloudRot_involutive` +- `ConstraintGraph.cloudRot_getElem` — a cloud, read through its enumeration, is + a copy of the family's expander +- `ConstraintGraph.order_reduceGraph`, `deg_reduceGraph` — size `2 · numEdges`, + degree `1 + E.degree` +- `ConstraintGraph.satisfiable_reduce_of_satisfiable` — completeness +-/ + +@[expose] public section + +namespace Complexity + +namespace ConstraintGraph + +variable {α : Type} (G : ConstraintGraph α) + +/-! ### Half-edges and clouds -/ + +/-- A half-edge: an edge together with one of its two endpoints (`false` is the +tail, `true` the head). These are the vertices of the reduced graph. -/ +abbrev HalfEdge (G : ConstraintGraph α) : Type := Fin G.numEdges × Bool + +/-- The vertex a half-edge is attached to. -/ +def owner (p : G.HalfEdge) : Fin G.numVerts := if p.2 then G.head p.1 else G.tail p.1 + +/-- The other half of the same edge. -/ +def flipHalf (p : G.HalfEdge) : G.HalfEdge := (p.1, !p.2) + +theorem flipHalf_involutive : Function.Involutive G.flipHalf := by + intro p + simp [flipHalf] + +/-- The half-edges attached to `v`. -/ +def cloud (v : Fin G.numVerts) : Finset G.HalfEdge := + Finset.univ.filter fun p => G.owner p = v + +@[simp] theorem mem_cloud {v : Fin G.numVerts} {p : G.HalfEdge} : + p ∈ G.cloud v ↔ G.owner p = v := by + simp [cloud] + +/-- The number naming a half-edge. The head of an edge is numbered before its +tail, which is the order `Fintype` enumerates `Bool` in. -/ +def halfCode (p : G.HalfEdge) : ℕ := 2 * p.1.val + (if p.2 then 0 else 1) + +theorem halfCode_injective : Function.Injective G.halfCode := by + rintro ⟨e, b⟩ ⟨e', b'⟩ h + simp only [halfCode] at h + have he : e.val = e'.val := by cases b <;> cases b' <;> simp at h <;> omega + have hb : b = b' := by cases b <;> cases b' <;> simp at h ⊢ <;> omega + rw [Fin.ext he, hb] + +instance halfLE_trans : + IsTrans G.HalfEdge (fun p q => G.halfCode p ≤ G.halfCode q) := ⟨fun _ _ _ => le_trans⟩ + +instance halfLE_antisymm : + Std.Antisymm (fun p q : G.HalfEdge => G.halfCode p ≤ G.halfCode q) := + ⟨fun _ _ h1 h2 => G.halfCode_injective (le_antisymm h1 h2)⟩ + +instance halfLE_total : + Std.Total (fun p q : G.HalfEdge => G.halfCode p ≤ G.halfCode q) := + ⟨fun _ _ => le_total _ _⟩ + +/-- The half-edges of a cloud, listed without repetition, in order of their +numbers — the order an algorithm can find them in. -/ +noncomputable def cloudList (v : Fin G.numVerts) : List G.HalfEdge := + (G.cloud v).sort (fun p q => G.halfCode p ≤ G.halfCode q) + +theorem nodup_cloudList (v : Fin G.numVerts) : (G.cloudList v).Nodup := + Finset.sort_nodup _ _ + +theorem pairwise_cloudList (v : Fin G.numVerts) : + List.Pairwise (fun p q => G.halfCode p ≤ G.halfCode q) (G.cloudList v) := + Finset.pairwise_sort _ _ + +@[simp] theorem mem_cloudList {v : Fin G.numVerts} {p : G.HalfEdge} : + p ∈ G.cloudList v ↔ p ∈ G.cloud v := Finset.mem_sort _ + +theorem mem_cloudList_self (p : G.HalfEdge) : p ∈ G.cloudList (G.owner p) := by simp + +/-! ### The cloud rotation -/ + +variable (E : ExpanderFamily) + +/-- The cloud-link rotation, with the cloud's enumeration passed in explicitly +so that no dependent rewriting is ever needed. -/ +noncomputable def cloudRotAux (l : List G.HalfEdge) (p : G.HalfEdge) (j : Fin E.degree) : + G.HalfEdge × Fin E.degree := + if h : l.idxOf p < l.length then + let q := E.rot l.length (⟨l.idxOf p, h⟩, j) + (l.getD q.1.val p, q.2) + else (p, j) + +/-- Inside one cloud, the rotation is an involution: this is the family's own +involutivity, transported through the enumeration. -/ +theorem cloudRotAux_involutive {l : List G.HalfEdge} (hnd : l.Nodup) {p : G.HalfEdge} + (hp : p ∈ l) (j : Fin E.degree) : + G.cloudRotAux E l (G.cloudRotAux E l p j).1 (G.cloudRotAux E l p j).2 = (p, j) := by + have hlt : l.idxOf p < l.length := List.idxOf_lt_length_iff.mpr hp + set q := E.rot l.length (⟨l.idxOf p, hlt⟩, j) with hq + have hq1 : q.1.val < l.length := q.1.isLt + have hstep : G.cloudRotAux E l p j = (l.getD q.1.val p, q.2) := by + rw [cloudRotAux, dif_pos hlt] + have hget : l.getD q.1.val p = l[q.1.val] := (List.getElem_eq_getD p).symm + have hidx' : l.idxOf l[q.1.val] = q.1.val := hnd.idxOf_getElem _ hq1 + have hlt' : l.idxOf (l.getD q.1.val p) < l.length := by + rw [hget, hidx']; exact hq1 + rw [hstep, cloudRotAux, dif_pos hlt'] + have hval : l.idxOf (l.getD q.1.val p) = q.1.val := by rw [hget, hidx'] + have hfin : (⟨l.idxOf (l.getD q.1.val p), hlt'⟩ : Fin l.length) = q.1 := Fin.ext hval + rw [hfin] + have hinv : E.rot l.length (q.1, q.2) = (⟨l.idxOf p, hlt⟩, j) := by + rw [hq, Prod.mk.eta, E.rot_involutive l.length] + rw [hinv] + have hself : ∀ d : G.HalfEdge, l.getD (l.idxOf p) d = p := by + intro d + rw [← List.getElem_eq_getD (h := hlt), List.getElem_idxOf hlt] + exact Prod.ext (hself _) rfl + +/-- The cloud-link rotation at a half-edge, using its own cloud. -/ +noncomputable def cloudRot (p : G.HalfEdge) (j : Fin E.degree) : G.HalfEdge × Fin E.degree := + G.cloudRotAux E (G.cloudList (G.owner p)) p j + +/-- A cloud-link lands inside the enumeration it started from. -/ +theorem cloudRotAux_mem {l : List G.HalfEdge} {p : G.HalfEdge} (hp : p ∈ l) + (j : Fin E.degree) : (G.cloudRotAux E l p j).1 ∈ l := by + have hlt : l.idxOf p < l.length := List.idxOf_lt_length_iff.mpr hp + have hq1 : (E.rot l.length (⟨l.idxOf p, hlt⟩, j)).1.val < l.length := + (E.rot l.length (⟨l.idxOf p, hlt⟩, j)).1.isLt + rw [cloudRotAux, dif_pos hlt] + dsimp only + rw [← List.getElem_eq_getD (h := hq1)] + exact List.getElem_mem hq1 + +/-- A cloud-link stays inside the cloud. -/ +theorem owner_cloudRot (p : G.HalfEdge) (j : Fin E.degree) : + G.owner (G.cloudRot E p j).1 = G.owner p := by + have h := G.cloudRotAux_mem E (G.mem_cloudList_self p) j + rw [cloudRot] + simpa using h + +/-- **The cloud is a copy of the expander.** Reading the cloud through its +enumeration, a cloud-link is exactly the family's rotation map. This is what +lets the expander estimates of `Disagreement` be applied to a cloud. -/ +theorem cloudRot_getElem (v : Fin G.numVerts) (i : Fin (G.cloudList v).length) + (j : Fin E.degree) : + (G.cloudRot E ((G.cloudList v)[i.val]) j).1 + = (G.cloudList v)[(E.rot (G.cloudList v).length (i, j)).1.val] := by + have hmem : (G.cloudList v)[i.val] ∈ G.cloudList v := List.getElem_mem i.isLt + have howner : G.owner ((G.cloudList v)[i.val]) = v := + (G.mem_cloud).mp ((G.mem_cloudList).mp hmem) + have hidx : (G.cloudList v).idxOf ((G.cloudList v)[i.val]) = i.val := + (G.nodup_cloudList v).idxOf_getElem _ i.isLt + have hlt : (G.cloudList v).idxOf ((G.cloudList v)[i.val]) < (G.cloudList v).length := by + rw [hidx]; exact i.isLt + rw [cloudRot, howner, cloudRotAux, dif_pos hlt] + have hfin : (⟨(G.cloudList v).idxOf ((G.cloudList v)[i.val]), hlt⟩ : + Fin (G.cloudList v).length) = i := Fin.ext hidx + rw [hfin] + dsimp only + exact (List.getElem_eq_getD _).symm + +theorem cloudRot_involutive (p : G.HalfEdge) (j : Fin E.degree) : + G.cloudRot E (G.cloudRot E p j).1 (G.cloudRot E p j).2 = (p, j) := by + have howner : G.owner (G.cloudRot E p j).1 = G.owner p := G.owner_cloudRot E p j + rw [cloudRot, howner] + exact G.cloudRotAux_involutive E (G.nodup_cloudList _) (G.mem_cloudList_self p) j + +/-! ### The reduced graph -/ + +/-- The reduced graph: every half-edge has one edge-link to the other half of +its edge, and `E.degree` cloud-links to the half-edges sharing its endpoint. -/ +noncomputable def reduceGraph (G : ConstraintGraph α) (E : ExpanderFamily) : RegGraph where + V := G.HalfEdge + D := Option (Fin E.degree) + decEqV := inferInstance + decEqD := inferInstance + fintypeV := inferInstance + fintypeD := inferInstance + nonemptyD := ⟨none⟩ + rot x := + match x.2 with + | none => (G.flipHalf x.1, none) + | some j => ((G.cloudRot E x.1 j).1, some (G.cloudRot E x.1 j).2) + rot_involutive := by + rintro ⟨p, _ | j⟩ + · show (G.flipHalf (G.flipHalf p), (none : Option (Fin E.degree))) = (p, none) + rw [G.flipHalf_involutive p] + · show ((G.cloudRot E (G.cloudRot E p j).1 (G.cloudRot E p j).2).1, + some (G.cloudRot E (G.cloudRot E p j).1 (G.cloudRot E p j).2).2) = (p, some j) + rw [G.cloudRot_involutive E p j] + +@[simp] theorem V_reduceGraph : (G.reduceGraph E).V = G.HalfEdge := rfl + +@[simp] theorem order_reduceGraph : (G.reduceGraph E).order = 2 * G.numEdges := by + show Fintype.card (Fin G.numEdges × Bool) = 2 * G.numEdges + simp [Nat.mul_comm] + +/-- The reduced graph is `(1 + E.degree)`-regular. -/ +@[simp] theorem deg_reduceGraph : (G.reduceGraph E).deg = 1 + E.degree := by + show Fintype.card (Option (Fin E.degree)) = 1 + E.degree + simp [Nat.add_comm] + +theorem nbr_reduceGraph_none (p : G.HalfEdge) : + (G.reduceGraph E).nbr p none = G.flipHalf p := rfl + +theorem nbr_reduceGraph_some (p : G.HalfEdge) (j : Fin E.degree) : + (G.reduceGraph E).nbr p (some j) = (G.cloudRot E p j).1 := rfl + +/-! ### The reduced constraint system -/ + +variable [DecidableEq α] + +/-- The reduced constraint system: an edge-link carries the original constraint +of its edge, oriented so that the tail's label comes first, and a cloud-link +demands that the two half-edges agree. -/ +noncomputable def reduce (G : ConstraintGraph α) (E : ExpanderFamily) : RegCSP α where + graph := G.reduceGraph E + rel p d a b := + match d with + | none => if p.2 then G.rel p.1 b a else G.rel p.1 a b + | some _ => a == b + +@[simp] theorem graph_reduce : (G.reduce E).graph = G.reduceGraph E := rfl + +/-- The reduced system's vertices are the half-edges, numbered by their edge and +their side. -/ +noncomputable instance : NumEnc (G.reduce E).graph.V := + inferInstanceAs (NumEnc (Fin G.numEdges × Bool)) + +/-- Completeness: labelling every half-edge by its endpoint's label carries a +satisfying assignment of `G` to one of the reduced system. -/ +theorem satisfiable_reduce_of_satisfiable (h : G.Satisfiable) : (G.reduce E).Satisfiable := by + obtain ⟨σ, hσ⟩ := h + refine ⟨fun p => σ (G.owner p), ?_⟩ + rintro ⟨p, _ | j⟩ + · -- an edge-link carries the original constraint + rw [RegCSP.Satisfies, RegCSP.satisfies] + dsimp only + show (if p.2 then G.rel p.1 (σ (G.owner ((G.reduceGraph E).nbr p none))) (σ (G.owner p)) + else G.rel p.1 (σ (G.owner p)) (σ (G.owner ((G.reduceGraph E).nbr p none)))) = true + rw [nbr_reduceGraph_none] + have hedge := hσ p.1 + rw [Satisfies, satisfies] at hedge + by_cases hb : p.2 = true + · have h1 : G.owner p = G.head p.1 := by simp [owner, hb] + have h2 : G.owner (G.flipHalf p) = G.tail p.1 := by simp [owner, flipHalf, hb] + rw [if_pos hb, h1, h2] + exact hedge + · have hb' : p.2 = false := by simpa using hb + have h1 : G.owner p = G.tail p.1 := by simp [owner, hb'] + have h2 : G.owner (G.flipHalf p) = G.head p.1 := by simp [owner, flipHalf, hb'] + rw [if_neg hb, h1, h2] + exact hedge + · -- a cloud-link joins half-edges with the same endpoint + rw [RegCSP.Satisfies, RegCSP.satisfies] + dsimp only + show ((σ (G.owner p) == σ (G.owner ((G.reduceGraph E).nbr p (some j)))) = true) + rw [nbr_reduceGraph_some, G.owner_cloudRot E p j] + simp + +end ConstraintGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/DegreeReductionSound.lean b/Complexitylib/Classes/PCP/Internal/DegreeReductionSound.lean new file mode 100644 index 00000000..439b04b2 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/DegreeReductionSound.lean @@ -0,0 +1,294 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.CloudDisagreement + +/-! +# Soundness of degree reduction + +Completeness was proved in `DegreeReduction`; this is the other half. An +assignment `A` of the reduced system is decoded to a vertex assignment by +plurality, and the unsatisfied darts of `A` are charged against the edges that +decoding fails: + +* **cloud-links** pay for every half-edge that disagrees with its vertex's + plurality label (`CloudDisagreement.total_cloud_charge`), and +* **edge-links** pay for every original edge that the decoded assignment fails + *and* whose two half-edges both agree — for such an edge the edge-link carries + exactly the failed original constraint. + +An edge escapes the second bill only by having a disagreeing half-edge, and +those are already billed by the first. Trading the two off gives a bound of +`min 1 c` times the original unsatisfied fraction, with +`c = (1 - lam) · degree / card α`. + +## Main definitions + +- `ConstraintGraph.cloudDarts`, `edgeDarts` — the unsatisfied darts split by + which kind of link they are +- `ConstraintGraph.goodEdges` — the failed original edges whose halves agree + +## Main results + +- `ConstraintGraph.card_unsatEdges_le_charge` — an edge failed by decoding is either + billed to an edge-link or has a disagreeing half-edge +- `ConstraintGraph.card_unsatDarts_ge` — the combined charge +- `ConstraintGraph.unsatFrac_reduce_ge`, `ConstraintGraph.le_unsatVal_reduce` — + soundness: the reduced system's unsatisfied fraction, and its value, are at + least `reduceConst` times `G`'s value +-/ + +@[expose] public section + +namespace Complexity + +namespace ConstraintGraph + +/-- The constant factor degree reduction costs: the trade-off between the cloud +charge `(1 - lam) · degree / card α` and the edge charge `1`, diluted by the +`(1 + degree)`-fold increase in darts. -/ +noncomputable def reduceConst (E : ExpanderFamily) (α : Type) [Fintype α] : ℝ := + min 1 ((1 - E.lam) * (E.degree : ℝ) / (Fintype.card α : ℝ)) / (2 * (1 + (E.degree : ℝ))) + +variable {α : Type} [Fintype α] [DecidableEq α] [Nonempty α] +variable (G : ConstraintGraph α) (E : ExpanderFamily) + +/-! ### Splitting the unsatisfied darts -/ + +/-- The unsatisfied cloud-links. -/ +noncomputable def cloudDarts (A : (G.reduce E).Assignment) : Finset (G.reduce E).Dart := + ((G.reduce E).unsatDarts A).filter fun x => x.2 ≠ none + +/-- The unsatisfied edge-links. -/ +noncomputable def edgeDarts (A : (G.reduce E).Assignment) : Finset (G.reduce E).Dart := + ((G.reduce E).unsatDarts A).filter fun x => x.2 = none + +omit [Fintype α] [Nonempty α] in +theorem card_cloudDarts_add_card_edgeDarts (A : (G.reduce E).Assignment) : + (G.cloudDarts E A).card + (G.edgeDarts E A).card = ((G.reduce E).unsatDarts A).card := by + rw [cloudDarts, edgeDarts, Nat.add_comm] + exact Finset.card_filter_add_card_filter_not (s := (G.reduce E).unsatDarts A) + (p := fun x : (G.reduce E).Dart => x.2 = none) + +omit [Fintype α] [Nonempty α] in +theorem cloudUnsat_subset_cloudDarts (A : (G.reduce E).Assignment) (v : Fin G.numVerts) : + G.cloudUnsat E A v ⊆ G.cloudDarts E A := by + intro x hx + rw [cloudUnsat, Finset.mem_filter] at hx + rw [cloudDarts, Finset.mem_filter] + exact ⟨hx.1, hx.2.2⟩ + +/-- The cloud charge lands entirely among the unsatisfied cloud-links. -/ +theorem cloud_charge_le_card_cloudDarts (A : (G.reduce E).Assignment) : + (1 - E.lam) * (E.degree : ℝ) * ((G.devSet A).card : ℝ) / (Fintype.card α : ℝ) + ≤ ((G.cloudDarts E A).card : ℝ) := by + have hsum : ∑ v : Fin G.numVerts, + ((1 - E.lam) * (E.degree : ℝ) * ((G.devIdx A v).card : ℝ) / (Fintype.card α : ℝ)) + ≤ ∑ v : Fin G.numVerts, ((G.cloudUnsat E A v).card : ℝ) := + Finset.sum_le_sum fun v _ => G.cloud_disagreement_bound E A v + have hleft : ∑ v : Fin G.numVerts, + ((1 - E.lam) * (E.degree : ℝ) * ((G.devIdx A v).card : ℝ) / (Fintype.card α : ℝ)) + = (1 - E.lam) * (E.degree : ℝ) * ((G.devSet A).card : ℝ) / (Fintype.card α : ℝ) := by + rw [← Finset.sum_div, ← Finset.mul_sum] + congr 2 + rw [← Nat.cast_sum, G.sum_card_devIdx A] + have hdisj : ∀ u ∈ (Finset.univ : Finset (Fin G.numVerts)), + ∀ v ∈ (Finset.univ : Finset (Fin G.numVerts)), u ≠ v → + Disjoint (G.cloudUnsat E A u) (G.cloudUnsat E A v) := + fun u _ v _ h => G.cloudUnsat_disjoint E A h + have hnat : ∑ v : Fin G.numVerts, (G.cloudUnsat E A v).card ≤ (G.cloudDarts E A).card := by + rw [← Finset.card_biUnion hdisj] + refine Finset.card_le_card ?_ + intro x hx + rw [Finset.mem_biUnion] at hx + obtain ⟨v, -, hxv⟩ := hx + exact G.cloudUnsat_subset_cloudDarts E A v hxv + have hright : ∑ v : Fin G.numVerts, ((G.cloudUnsat E A v).card : ℝ) + ≤ ((G.cloudDarts E A).card : ℝ) := by + rw [← Nat.cast_sum] + exact_mod_cast hnat + rw [← hleft] + exact le_trans hsum hright + +/-! ### The edge-link charge -/ + +/-- The original edges that decoding fails and whose two half-edges both agree +with their vertices' labels. -/ +noncomputable def goodEdges (A : (G.reduce E).Assignment) : Finset (Fin G.numEdges) := + (G.unsatEdges (G.decode A)).filter fun e => + (e, false) ∉ G.devSet A ∧ (e, true) ∉ G.devSet A + +/-- Each such edge contributes an unsatisfied edge-link: the link carries +exactly the original constraint, evaluated at the decoded labels. -/ +theorem card_goodEdges_le (A : (G.reduce E).Assignment) : + (G.goodEdges E A).card ≤ (G.edgeDarts E A).card := by + refine Finset.card_le_card_of_injOn (fun e => ((e, false), none)) ?_ ?_ + · intro e he + simp only [Finset.mem_coe] at he ⊢ + rw [goodEdges, Finset.mem_filter] at he + obtain ⟨hfail, h0, h1⟩ := he + have hA0 : A (e, false) = G.decode A (G.tail e) := by + have h := (G.mem_devSet (A := A) (p := (e, false))).not.mp h0 + simp only [not_not] at h + have howner : G.owner ((e, false) : G.HalfEdge) = G.tail e := by simp [owner] + rw [h, howner] + have hA1 : A (e, true) = G.decode A (G.head e) := by + have h := (G.mem_devSet (A := A) (p := (e, true))).not.mp h1 + simp only [not_not] at h + have howner : G.owner ((e, true) : G.HalfEdge) = G.head e := by simp [owner] + rw [h, howner] + rw [edgeDarts, Finset.mem_filter] + refine ⟨?_, rfl⟩ + rw [RegCSP.mem_unsatDarts] + show ¬ ((if ((e, false) : G.HalfEdge).2 then + G.rel e (A ((G.reduce E).graph.nbr (e, false) none)) (A (e, false)) + else G.rel e (A (e, false)) (A ((G.reduce E).graph.nbr (e, false) none))) = true) + have hnbr : (G.reduce E).graph.nbr ((e, false) : G.HalfEdge) none = (e, true) := by + show G.flipHalf (e, false) = (e, true) + simp [flipHalf] + rw [hnbr] + simp only [if_neg (by simp : ¬ (((e, false) : G.HalfEdge).2 = true))] + rw [hA0, hA1] + rw [mem_unsatEdges] at hfail + rw [Satisfies, satisfies] at hfail + exact hfail + · intro e _ f _ hef + have := congrArg (fun x => x.1.1) hef + simpa using this + +/-- An edge that decoding fails is either billed to an edge-link or has a +disagreeing half-edge. -/ +theorem card_unsatEdges_le_charge (A : (G.reduce E).Assignment) : + (G.unsatEdges (G.decode A)).card ≤ (G.goodEdges E A).card + (G.devSet A).card := by + have hsub : G.unsatEdges (G.decode A) + ⊆ G.goodEdges E A ∪ (G.devSet A).image Prod.fst := by + intro e he + by_cases hgood : (e, false) ∉ G.devSet A ∧ (e, true) ∉ G.devSet A + · exact Finset.mem_union_left _ (by rw [goodEdges, Finset.mem_filter]; exact ⟨he, hgood⟩) + · refine Finset.mem_union_right _ ?_ + rw [Finset.mem_image] + rw [not_and_or, not_not, not_not] at hgood + rcases hgood with h | h + · exact ⟨(e, false), h, rfl⟩ + · exact ⟨(e, true), h, rfl⟩ + calc (G.unsatEdges (G.decode A)).card + ≤ (G.goodEdges E A ∪ (G.devSet A).image Prod.fst).card := Finset.card_le_card hsub + _ ≤ (G.goodEdges E A).card + ((G.devSet A).image Prod.fst).card := Finset.card_union_le _ _ + _ ≤ (G.goodEdges E A).card + (G.devSet A).card := by + exact Nat.add_le_add_left (Finset.card_image_le) _ + +/-! ### Soundness -/ + +/-- **The combined charge.** With `c = (1 - lam) · degree / card α`, the +unsatisfied darts number at least `min 1 c` times the edges that decoding +fails. -/ +theorem card_unsatDarts_ge (A : (G.reduce E).Assignment) : + min 1 ((1 - E.lam) * (E.degree : ℝ) / (Fintype.card α : ℝ)) + * ((G.unsatEdges (G.decode A)).card : ℝ) + ≤ (((G.reduce E).unsatDarts A).card : ℝ) := by + set c : ℝ := (1 - E.lam) * (E.degree : ℝ) / (Fintype.card α : ℝ) with hc + set D : ℝ := ((G.devSet A).card : ℝ) with hD + set U : ℝ := ((G.unsatEdges (G.decode A)).card : ℝ) with hU + have hcloud : c * D ≤ ((G.cloudDarts E A).card : ℝ) := by + have h := G.cloud_charge_le_card_cloudDarts E A + rw [hc, hD] + calc (1 - E.lam) * (E.degree : ℝ) / (Fintype.card α : ℝ) * ((G.devSet A).card : ℝ) + = (1 - E.lam) * (E.degree : ℝ) * ((G.devSet A).card : ℝ) / (Fintype.card α : ℝ) := by + ring + _ ≤ ((G.cloudDarts E A).card : ℝ) := h + have hedge : U ≤ ((G.edgeDarts E A).card : ℝ) + D := by + have h1 := G.card_unsatEdges_le_charge E A + have h2 := G.card_goodEdges_le E A + have h1R : U ≤ ((G.goodEdges E A).card : ℝ) + D := by rw [hU, hD]; exact_mod_cast h1 + have h2R : ((G.goodEdges E A).card : ℝ) ≤ ((G.edgeDarts E A).card : ℝ) := by + exact_mod_cast h2 + linarith + have htotal : ((G.cloudDarts E A).card : ℝ) + ((G.edgeDarts E A).card : ℝ) + = (((G.reduce E).unsatDarts A).card : ℝ) := by + rw [← Nat.cast_add, G.card_cloudDarts_add_card_edgeDarts E A] + have hDnn : 0 ≤ D := by rw [hD]; positivity + have hcnn : 0 ≤ c := by + rw [hc] + have h1 : 0 ≤ 1 - E.lam := by linarith [E.lam_lt_one] + positivity + rcases le_total 1 c with hcge | hcle + · have hmin : min 1 c = 1 := min_eq_left hcge + rw [hmin, one_mul, ← htotal] + nlinarith [hcloud, hedge] + · have hmin : min 1 c = c := min_eq_right hcle + rw [hmin, ← htotal] + nlinarith [hcloud, hedge, hDnn, hcnn] + +/-- **Soundness of degree reduction.** The reduced system's unsatisfied +fraction is at least a constant times the original's unsatisfiability value, +the constant depending only on the alphabet size and the cloud expander. -/ +theorem unsatFrac_reduce_ge (A : (G.reduce E).Assignment) : + min 1 ((1 - E.lam) * (E.degree : ℝ) / (Fintype.card α : ℝ)) / (2 * (1 + (E.degree : ℝ))) + * ((G.unsatVal : ℚ) : ℝ) + ≤ (((G.reduce E).unsatFrac A : ℚ) : ℝ) := by + set c : ℝ := min 1 ((1 - E.lam) * (E.degree : ℝ) / (Fintype.card α : ℝ)) with hc + have hcnn : 0 ≤ c := by + rw [hc] + refine le_min zero_le_one ?_ + have h1 : 0 ≤ 1 - E.lam := by linarith [E.lam_lt_one] + positivity + have hdart : ((G.reduce E).graph.order * (G.reduce E).graph.deg : ℕ) + = 2 * G.numEdges * (1 + E.degree) := by + show ((G.reduceGraph E).order * (G.reduceGraph E).deg : ℕ) = _ + rw [G.order_reduceGraph E, G.deg_reduceGraph E] + rcases Nat.eq_zero_or_pos G.numEdges with hm | hm + · have hsat : G.Satisfiable := by + by_contra hcon + have := G.numEdges_pos_of_not_satisfiable hcon + omega + have h0 : G.unsatVal = 0 := (G.unsatVal_eq_zero_iff_satisfiable).mpr hsat + rw [h0] + simp only [Rat.cast_zero, mul_zero] + have : (0 : ℚ) ≤ (G.reduce E).unsatFrac A := (G.reduce E).unsatFrac_nonneg A + exact_mod_cast this + · have hmq : (0 : ℝ) < (G.numEdges : ℝ) := by exact_mod_cast hm + have hfrac : (((G.reduce E).unsatFrac A : ℚ) : ℝ) + = (((G.reduce E).unsatDarts A).card : ℝ) + / (2 * (G.numEdges : ℝ) * (1 + (E.degree : ℝ))) := by + rw [RegCSP.unsatFrac, hdart] + push_cast + ring + have hU : ((G.unsatVal : ℚ) : ℝ) * (G.numEdges : ℝ) + ≤ ((G.unsatEdges (G.decode A)).card : ℝ) := by + have h := G.unsatVal_le (G.decode A) + rw [unsatFrac] at h + have hR : ((G.unsatVal : ℚ) : ℝ) + ≤ ((G.unsatEdges (G.decode A)).card : ℝ) / (G.numEdges : ℝ) := by + have hcast := (Rat.cast_le (K := ℝ)).mpr h + push_cast at hcast + exact hcast + rwa [le_div_iff₀ hmq] at hR + have hcharge := G.card_unsatDarts_ge E A + have hkey : c * ((G.unsatVal : ℚ) : ℝ) * (G.numEdges : ℝ) + ≤ (((G.reduce E).unsatDarts A).card : ℝ) := by + calc c * ((G.unsatVal : ℚ) : ℝ) * (G.numEdges : ℝ) + = c * (((G.unsatVal : ℚ) : ℝ) * (G.numEdges : ℝ)) := by ring + _ ≤ c * ((G.unsatEdges (G.decode A)).card : ℝ) := + mul_le_mul_of_nonneg_left hU hcnn + _ ≤ (((G.reduce E).unsatDarts A).card : ℝ) := hcharge + have hden : (0 : ℝ) < 2 * (G.numEdges : ℝ) * (1 + (E.degree : ℝ)) := by positivity + rw [hfrac, le_div_iff₀ hden] + calc c / (2 * (1 + (E.degree : ℝ))) * ((G.unsatVal : ℚ) : ℝ) + * (2 * (G.numEdges : ℝ) * (1 + (E.degree : ℝ))) + = c * ((G.unsatVal : ℚ) : ℝ) * (G.numEdges : ℝ) := by + field_simp + _ ≤ (((G.reduce E).unsatDarts A).card : ℝ) := hkey + +/-- **Soundness of degree reduction, on values.** -/ +theorem le_unsatVal_reduce : + reduceConst E α * ((G.unsatVal : ℚ) : ℝ) ≤ (((G.reduce E).unsatVal : ℚ) : ℝ) := by + obtain ⟨A, hA⟩ := (G.reduce E).exists_assignment_unsatFrac_eq_unsatVal + rw [reduceConst, ← hA] + exact G.unsatFrac_reduce_ge E A + +end ConstraintGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/Dinur.lean b/Complexitylib/Classes/PCP/Internal/Dinur.lean new file mode 100644 index 00000000..a6068664 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/Dinur.lean @@ -0,0 +1,361 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.Compose +public import Complexitylib.Classes.PCP.Internal.PoweringBound +public import Complexitylib.Classes.PCP.Internal.Preprocess +public import Complexitylib.Classes.PCP.Internal.Amplification + +/-! +# One round of Dinur's amplification + +Preprocess, power, compose: the three steps assembled into a single +transformation of constraint graphs over a fixed alphabet, packaged as an +`Amplifier`. Given an expander family, the round + +* preserves satisfiability, +* multiplies the number of edges by a constant, and +* at least doubles the unsatisfiability value until it reaches a universal + threshold. + +The alphabet is the one the composition produces, `Alpha ReadIdx`. Powering +blows it up to `KOpinion`, whose size is a fixed function of the powering +length and the preprocessed degree; composition brings it back. + +The only free parameter is the killing rate `q`. The powering bound's slope +grows linearly in `q` while every other loss — preprocessing, composition — +is a constant, so a large enough `q` makes the round double the value. +`q` is chosen by an Archimedean argument, and the threshold is any rational +below the powering bound's floor. + +## Main definitions + +- `Complexity.DinurAlpha` — the fixed alphabet +- `Complexity.Dinur.step` — one round +- `Complexity.Dinur.amplifier` — the round as an `Amplifier` + +## Main results + +- `Complexity.Dinur.numEdges_step` — the edge count grows by a constant factor +- `Complexity.Dinur.satisfiable_step` — completeness +- `Complexity.Dinur.min_le_unsatVal_step` — the value at least doubles, up to + the threshold +-/ + +@[expose] public section + +namespace Complexity + +open BooleanAnalysis + +/-- The fixed alphabet of the amplification: the composition's alphabet. -/ +abbrev DinurAlpha : Type := MultiTest.Alpha ReadIdx + +namespace Dinur + +variable (E : ExpanderFamily) + +/-! ### The powered alphabet's size -/ + +/-- The degree after preprocessing. -/ +def powDeg : ℕ := 2 + 2 * E.degree + +/-- The number of walks of length at most `T` out of a vertex. -/ +def walkCount (T : ℕ) : ℕ := ∑ ℓ ∈ Finset.range (T + 1), powDeg E ^ ℓ + +/-- The number of powered labels, as bits of a one-hot encoding. -/ +def bits (T : ℕ) : ℕ := Fintype.card DinurAlpha ^ walkCount E T + +theorem card_varWalk (G : ConstraintGraph DinurAlpha) (T : ℕ) : + Fintype.card (VarWalk (G.preprocess E).graph T) = walkCount E T := by + have hD : Fintype.card (G.preprocess E).graph.D = powDeg E := by + have := G.deg_preprocess E + rw [RegGraph.deg] at this + exact this + rw [walkCount, ← Fin.sum_univ_eq_sum_range (fun ℓ => powDeg E ^ ℓ) (T + 1)] + show Fintype.card (Σ ℓ : Fin (T + 1), Fin ℓ.val → (G.preprocess E).graph.D) = _ + rw [Fintype.card_sigma] + refine Finset.sum_congr rfl fun ℓ _ => ?_ + rw [Fintype.card_fun, Fintype.card_fin, hD] + +theorem card_kOpinion (G : ConstraintGraph DinurAlpha) (T : ℕ) : + Fintype.card (KOpinion (G.preprocess E).graph T DinurAlpha) = bits E T := by + show Fintype.card (VarWalk (G.preprocess E).graph T → DinurAlpha) = _ + rw [Fintype.card_fun, card_varWalk, bits] + +/-! ### The encoding -/ + +theorem basisVec_injective (n : ℕ) : Function.Injective (basisVec (n := n)) := by + intro i j hij + have h := congrFun hij i + by_contra hne + simp only [basisVec, if_true, if_neg hne] at h + exact absurd h (by decide) + +/-- The encoding of a powered label: the basis vector at its index. -/ +noncomputable def enc (G : ConstraintGraph DinurAlpha) (T : ℕ) + (σ : KOpinion (G.preprocess E).graph T DinurAlpha) : Cube (bits E T) := + basisVec (Fin.cast (card_kOpinion E G T) (Fintype.equivFin _ σ)) + +theorem enc_injective (G : ConstraintGraph DinurAlpha) (T : ℕ) : + Function.Injective (enc E G T) := by + intro σ τ h + have h1 := basisVec_injective _ h + have h2 := Fin.cast_injective _ h1 + exact (Fintype.equivFin _).injective h2 + +/-! ### One round -/ + +/-- The alphabet's size, the `K` of the powering parameters. + +Deliberately a `def` and not an `abbrev`. The alphabet has `2^23` symbols, and +`K` occurs inside `powT K q`, which is itself an exponent; letting a tactic +unfold `K` to a numeral there produces terms far too large to elaborate. -/ +def K : ℕ := Fintype.card DinurAlpha + +theorem one_le_K : 1 ≤ K := Fintype.card_pos + +theorem card_dinurAlpha_eq : Fintype.card DinurAlpha = K := rfl + +/-- **One round of amplification** with killing rate `q`. -/ +noncomputable def step (q : ℕ) (hq : 0 < q) (G : ConstraintGraph DinurAlpha) : + ConstraintGraph DinurAlpha := + (((G.preprocess E).killedPow q (powT K q) hq).compose (enc E G (powT K q))).toGraph + +/-- The constant factor by which a round multiplies the edge count. -/ +def edgeFactor (q : ℕ) : ℕ := + 2 * (powDeg E ^ powT K q * q ^ powT K q) * 2 ^ Tester.ROf (bits E (powT K q)) * 22 + +theorem numEdges_step (q : ℕ) (hq : 0 < q) (G : ConstraintGraph DinurAlpha) : + (step E q hq G).numEdges = edgeFactor E q * G.numEdges := by + rw [step, MultiTest.numEdges_toGraph, card_readIdx, RegCSP.card_dart, + RegCSP.card_dart_killedPow, G.order_preprocess, G.deg_preprocess, edgeFactor] + show 2 * G.numEdges * ((2 + 2 * E.degree) ^ powT K q * q ^ powT K q) + * 2 ^ Tester.ROf (bits E (powT K q)) * 22 = _ + rw [powDeg] + ring + +theorem satisfiable_step (q : ℕ) (hq : 0 < q) (G : ConstraintGraph DinurAlpha) + (h : G.Satisfiable) : (step E q hq G).Satisfiable := + RegCSP.satisfiable_compose _ _ + (RegCSP.satisfiable_killedPow_of_satisfiable _ _ _ _ + (G.satisfiable_preprocess_of_satisfiable E h)) + +/-! ### The value -/ + +theorem preprocessLam_nonneg : 0 ≤ ConstraintGraph.preprocessLam E := by + have := E.lam_nonneg + rw [ConstraintGraph.preprocessLam] + positivity + +theorem preprocessConst_pos : 0 < ConstraintGraph.preprocessConst E DinurAlpha := by + have hd : (0 : ℝ) < E.degree := by exact_mod_cast E.degree_pos + have hl : 0 < 1 - E.lam := by linarith [E.lam_lt_one] + rw [ConstraintGraph.preprocessConst, ConstraintGraph.reduceConst] + have : 0 < min (1 : ℝ) ((1 - E.lam) * (E.degree : ℝ) / (Fintype.card DinurAlpha : ℝ)) := by + apply lt_min one_pos + positivity + positivity + +/-- **The value of one round**, for a fixed `q ≥ 2` and any graph with an edge: +at least the `min` of the powering slope (times the preprocessing constant) +times the value, and the powering floor, all divided by the composition's +loss. -/ +theorem le_unsatVal_step (q : ℕ) (hq2 : 2 ≤ q) (G : ConstraintGraph DinurAlpha) + (hG : 0 < G.numEdges) : + min (RegCSP.powSlope (RegCSP.powConst q DinurAlpha) (powT K q) (ConstraintGraph.preprocessLam E) + * ConstraintGraph.preprocessConst E DinurAlpha * ((G.unsatVal : ℚ) : ℝ)) + (RegCSP.powFloor (RegCSP.powConst q DinurAlpha) (powT K q) + (ConstraintGraph.preprocessLam E)) / 704 + ≤ (((step E q (by omega) G).unsatVal : ℚ) : ℝ) := by + have hq : 0 < q := by omega + obtain ⟨hH, hHT, hsq, hloss⟩ := powering_params_spec one_le_K hq2 (G.preprocess E).graph.deg + have hn : 0 < (G.preprocess E).graph.order := by + rw [G.order_preprocess]; omega + have hpow := (G.preprocess E).le_unsatVal_killedPow_min q (powT K q) hq hH hHT hsq + (preprocessLam_nonneg E) (ConstraintGraph.preprocessLam_lt_one E) + (G.spectralBound_preprocess E) hn + (by omega) hloss + have hpre := G.le_unsatVal_preprocess E + have hcomp : (((((G.preprocess E).killedPow q (powT K q) hq).unsatVal : ℚ) : ℝ)) / 704 + ≤ (((step E q hq G).unsatVal : ℚ) : ℝ) := by + have := ((G.preprocess E).killedPow q (powT K q) hq).le_unsatVal_compose + (enc E G (powT K q)) (enc_injective E G (powT K q)) + have h' : ((((((G.preprocess E).killedPow q (powT K q) hq).unsatVal / 704 : ℚ)) : ℝ)) + ≤ (((step E q hq G).unsatVal : ℚ) : ℝ) := by exact_mod_cast this + push_cast at h' + exact h' + refine le_trans ?_ hcomp + refine div_le_div_of_nonneg_right ?_ (by norm_num) + refine le_trans ?_ hpow + refine min_le_min_right _ ?_ + rw [mul_assoc] + refine mul_le_mul_of_nonneg_left hpre ?_ + rw [RegCSP.powSlope] + have hl : 0 < 1 - ConstraintGraph.preprocessLam E := by + linarith [ConstraintGraph.preprocessLam_lt_one E] + have hc : 0 ≤ RegCSP.powConst q DinurAlpha := by + rw [RegCSP.powConst, card_dinurAlpha_eq] + have h1 : (1 : ℝ) ≤ q := by exact_mod_cast (by omega : 1 ≤ q) + apply div_nonneg + · linarith + · have hsq : (0 : ℝ) ≤ (K : ℝ) ^ 2 := sq_nonneg _ + linarith + have hT0 : (0 : ℝ) ≤ (powT K q : ℝ) := Nat.cast_nonneg _ + have hden : 0 ≤ RegCSP.powConst q DinurAlpha + 2 + 2 * (powT K q : ℝ) + / (1 - ConstraintGraph.preprocessLam E) := by + have := div_nonneg (mul_nonneg (by norm_num : (0 : ℝ) ≤ 2) hT0) hl.le + linarith + exact div_nonneg (pow_nonneg hc 2) hden + +/-! ### Choosing the killing rate -/ + +/-- **A killing rate that doubles the value.** -/ +theorem exists_q : ∃ q : ℕ, 2 ≤ q ∧ 1408 + ≤ RegCSP.powSlope (RegCSP.powConst q DinurAlpha) (powT K q) (ConstraintGraph.preprocessLam E) + * ConstraintGraph.preprocessConst E DinurAlpha := by + have hpc := preprocessConst_pos E + have hl1 := ConstraintGraph.preprocessLam_lt_one E + have hK0 : (0 : ℝ) < K := by exact_mod_cast one_le_K + have hs : 0 < slopeUnit (K : ℝ) (ConstraintGraph.preprocessLam E) := by + rw [slopeUnit] + have : 0 < 1 - ConstraintGraph.preprocessLam E := by linarith + positivity + obtain ⟨n, hn⟩ := exists_nat_ge (1408 / (slopeUnit (K : ℝ) (ConstraintGraph.preprocessLam E) + * ConstraintGraph.preprocessConst E DinurAlpha)) + refine ⟨n + 2, by omega, ?_⟩ + have hslope := slopeUnit_mul_le_powSlope (q := n + 2) one_le_K (by omega) hl1 + have hcast : (((n + 2 : ℕ) : ℝ) - 1) = (n : ℝ) + 1 := by push_cast; ring + rw [hcast] at hslope + have hpc' : RegCSP.powConst (n + 2) DinurAlpha = ((n : ℝ) + 1) / (4 * (K : ℝ) ^ 2) := by + rw [RegCSP.powConst, card_dinurAlpha_eq] + push_cast + ring + rw [hpc'] + rw [div_le_iff₀ (by positivity)] at hn + calc (1408 : ℝ) + ≤ n * (slopeUnit (K : ℝ) (ConstraintGraph.preprocessLam E) + * ConstraintGraph.preprocessConst E DinurAlpha) := hn + _ ≤ slopeUnit (K : ℝ) (ConstraintGraph.preprocessLam E) * ((n : ℝ) + 1) + * ConstraintGraph.preprocessConst E DinurAlpha := by + nlinarith [hs, hpc] + _ ≤ _ := mul_le_mul_of_nonneg_right hslope hpc.le + +/-- The chosen killing rate. -/ +noncomputable def q₀ : ℕ := Classical.choose (exists_q E) + +theorem two_le_q₀ : 2 ≤ q₀ E := (Classical.choose_spec (exists_q E)).1 + +theorem q₀_spec : 1408 + ≤ RegCSP.powSlope (RegCSP.powConst (q₀ E) DinurAlpha) (powT K (q₀ E)) + (ConstraintGraph.preprocessLam E) * ConstraintGraph.preprocessConst E DinurAlpha := + (Classical.choose_spec (exists_q E)).2 + +/-- The powering floor at the chosen rate. -/ +noncomputable def floor₀ : ℝ := + RegCSP.powFloor (RegCSP.powConst (q₀ E) DinurAlpha) (powT K (q₀ E)) + (ConstraintGraph.preprocessLam E) + +theorem floor₀_pos : 0 < floor₀ E := by + rw [floor₀, RegCSP.powFloor] + have hl : 0 < 1 - ConstraintGraph.preprocessLam E := by + linarith [ConstraintGraph.preprocessLam_lt_one E] + have hc : 0 < RegCSP.powConst (q₀ E) DinurAlpha := by + rw [RegCSP.powConst, card_dinurAlpha_eq] + have h2 : (2 : ℝ) ≤ q₀ E := by exact_mod_cast two_le_q₀ E + have hK0 : (0 : ℝ) < K := by exact_mod_cast one_le_K + apply div_pos + · linarith + · have hsq : (0 : ℝ) < (K : ℝ) ^ 2 := pow_pos hK0 2 + linarith + have hT : (0 : ℝ) < powT K (q₀ E) := by + have hpos : 0 < powT K (q₀ E) := by + rw [powT] + have h1 : 1 ≤ K := one_le_K + have h2 : 2 ≤ q₀ E := two_le_q₀ E + have h3 : 1 ≤ q₀ E - 1 := by omega + calc 0 < 2 * (4 * 1 * 1) := by norm_num + _ ≤ 2 * (4 * K * (q₀ E - 1)) := + Nat.mul_le_mul_left _ (Nat.mul_le_mul (Nat.mul_le_mul_left _ h1) h3) + exact_mod_cast hpos + have hT2 : 0 < (powT K (q₀ E) : ℝ) ^ 2 := pow_pos hT 2 + exact div_pos (div_pos (pow_pos hc 2) hT2) + (add_pos (add_pos hc (mul_pos two_pos hT2)) (div_pos (mul_pos two_pos hT) hl)) + +/-- **A rational threshold** below the floor (after the composition's loss) and +below one. -/ +theorem exists_gap : ∃ g : ℚ, 0 < g ∧ g ≤ 1 ∧ (g : ℝ) ≤ floor₀ E / 704 := by + have h : (0 : ℝ) < min (floor₀ E / 704) 1 := lt_min (by linarith [floor₀_pos E]) one_pos + obtain ⟨g, hg0, hg1⟩ := exists_rat_btwn h + refine ⟨g, by exact_mod_cast hg0, ?_, ?_⟩ + · have : (g : ℝ) ≤ 1 := le_trans hg1.le (min_le_right _ _) + exact_mod_cast this + · exact le_trans hg1.le (min_le_left _ _) + +/-- The chosen threshold. -/ +noncomputable def gap₀ : ℚ := Classical.choose (exists_gap E) + +theorem gap₀_pos : 0 < gap₀ E := (Classical.choose_spec (exists_gap E)).1 +theorem gap₀_le_one : gap₀ E ≤ 1 := (Classical.choose_spec (exists_gap E)).2.1 +theorem gap₀_le : ((gap₀ E : ℚ) : ℝ) ≤ floor₀ E / 704 := (Classical.choose_spec (exists_gap E)).2.2 + +/-- An edgeless graph has value zero. -/ +theorem unsatVal_eq_zero_of_numEdges_eq_zero (G : ConstraintGraph DinurAlpha) + (h : G.numEdges = 0) : G.unsatVal = 0 := by + refine le_antisymm ?_ G.unsatVal_nonneg + have a : G.Assignment := fun _ => Classical.arbitrary _ + refine le_trans (G.unsatVal_le a) ?_ + rw [ConstraintGraph.unsatFrac] + have hz : (G.numEdges : ℚ) = 0 := by rw [h]; norm_num + rw [hz, div_zero] + +/-- **The value at least doubles, up to the threshold.** -/ +theorem min_le_unsatVal_step (G : ConstraintGraph DinurAlpha) : + min (gap₀ E) (2 * G.unsatVal) ≤ (step E (q₀ E) (by have := two_le_q₀ E; omega) G).unsatVal := by + by_cases h0 : G.numEdges = 0 + · rw [unsatVal_eq_zero_of_numEdges_eq_zero G h0, mul_zero] + refine le_trans (min_le_right _ _) ?_ + exact ConstraintGraph.unsatVal_nonneg _ + have hG : 0 < G.numEdges := Nat.pos_of_ne_zero h0 + have hmain := le_unsatVal_step E (q₀ E) (two_le_q₀ E) G hG + have hq := q₀_spec E + have hv0 : (0 : ℝ) ≤ ((G.unsatVal : ℚ) : ℝ) := by exact_mod_cast G.unsatVal_nonneg + have hcast : (((min (gap₀ E) (2 * G.unsatVal) : ℚ)) : ℝ) + ≤ (((step E (q₀ E) (by have := two_le_q₀ E; omega) G).unsatVal : ℚ) : ℝ) := by + push_cast + refine le_trans ?_ hmain + rw [le_div_iff₀ (by norm_num : (0 : ℝ) < 704)] + have hgap := gap₀_le E + rw [floor₀] at hgap + refine le_min ?_ ?_ + · refine le_trans (mul_le_mul_of_nonneg_right (min_le_right _ _) (by norm_num)) ?_ + set s := RegCSP.powSlope (RegCSP.powConst (q₀ E) DinurAlpha) (powT K (q₀ E)) + (ConstraintGraph.preprocessLam E) with hs + set pc := ConstraintGraph.preprocessConst E DinurAlpha with hpc + have : (2 : ℝ) * ((G.unsatVal : ℚ) : ℝ) * 704 = 1408 * ((G.unsatVal : ℚ) : ℝ) := by ring + rw [this] + exact mul_le_mul_of_nonneg_right hq hv0 + · refine le_trans (mul_le_mul_of_nonneg_right (min_le_left _ _) (by norm_num)) ?_ + rw [le_div_iff₀ (by norm_num : (0 : ℝ) < 704)] at hgap + exact hgap + exact_mod_cast hcast + +/-! ### The amplifier -/ + +/-- **Dinur's round as an `Amplifier`.** -/ +noncomputable def amplifier : Amplifier DinurAlpha where + transform := step E (q₀ E) (by have := two_le_q₀ E; omega) + edgeFactor := edgeFactor E (q₀ E) + gap := gap₀ E + gap_pos := gap₀_pos E + gap_le_one := gap₀_le_one E + numEdges_transform_le := fun G => le_of_eq (numEdges_step E _ _ G) + satisfiable_transform := fun G h => satisfiable_step E _ _ G h + unsatVal_transform_ge := fun G => min_le_unsatVal_step E G + +end Dinur + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/EdgeExpansion.lean b/Complexitylib/Classes/PCP/Internal/EdgeExpansion.lean new file mode 100644 index 00000000..0f98ddb8 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/EdgeExpansion.lean @@ -0,0 +1,142 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.Mixing + +/-! +# Edge expansion from the spectral bound + +Dinur's degree-reduction step needs expansion in its *combinatorial* form: a +vertex set with few outgoing edges must be almost everything or almost nothing. +This module derives that from `Mixing` by feeding it indicator functions. + +Specialising `mixing_sq` to `1_S` and `1_T` says that the number of darts from +`S` to `T` is what independence predicts, `deg · |S| · |T| / n`, up to +`lam` times the two variances. Taking `T = Sᶜ`, both variances equal +`|S| |Sᶜ| / n`, so the error term's square root is rational in the data and no +`Real.sqrt` is needed: the number of darts leaving `S` is at least +`(1 - lam) · deg · |S| |Sᶜ| / n`. + +That is exactly the statement degree reduction consumes: inside a cloud built on +an expander, the vertices disagreeing with the cloud's plurality label send out +proportionally many edges, each of which is an unsatisfied equality constraint. + +## Main definitions + +- `RegGraph.dartsBetween` — the darts from one vertex set to another + +## Main results + +- `RegGraph.sum_indicator_mul_step` — the dart count as an operator inner + product +- `RegGraph.mixing_sq_indicator` — the mixing lemma for vertex sets +- `RegGraph.card_dartsBetween_compl_ge` — **edge expansion**: a set sends out at + least `(1 - lam) · deg · |S| |Sᶜ| / n` darts +-/ + +@[expose] public section + +namespace Complexity + +namespace RegGraph + +variable (G : RegGraph) + +/-- The darts whose tail lies in `S` and whose head lies in `T`. -/ +def dartsBetween (S T : Finset G.V) : Finset (G.V × G.D) := + Finset.univ.filter fun p => p.1 ∈ S ∧ G.nbr p.1 p.2 ∈ T + +/-- The real-valued indicator of a vertex set. -/ +def indicator (S : Finset G.V) : G.V → ℝ := + fun v => if v ∈ S then 1 else 0 + +@[simp] theorem sum_indicator (S : Finset G.V) : + ∑ v : G.V, G.indicator S v = (S.card : ℝ) := by + simp [indicator, Finset.sum_ite_mem] + +@[simp] theorem sum_sq_indicator (S : Finset G.V) : + ∑ v : G.V, (G.indicator S v) ^ 2 = (S.card : ℝ) := by + have h : ∀ v : G.V, (G.indicator S v) ^ 2 = G.indicator S v := by + intro v; by_cases hv : v ∈ S <;> simp [indicator, hv] + rw [Finset.sum_congr rfl fun v _ => h v, sum_indicator] + +/-- The dart count between two sets, as an inner product against the walk +operator. -/ +theorem sum_indicator_mul_step (S T : Finset G.V) : + ∑ v : G.V, G.indicator S v * G.step (G.indicator T) v + = ((G.dartsBetween S T).card : ℝ) / (G.deg : ℝ) := by + have hstep : ∀ v : G.V, G.indicator S v * G.step (G.indicator T) v + = (∑ i : G.D, G.indicator S v * G.indicator T (G.nbr v i)) / (G.deg : ℝ) := by + intro v + rw [step, mul_div_assoc', Finset.mul_sum] + rw [Finset.sum_congr rfl fun v _ => hstep v, ← Finset.sum_div] + congr 1 + have hprod : ∑ v : G.V, ∑ i : G.D, G.indicator S v * G.indicator T (G.nbr v i) + = ∑ p : G.V × G.D, G.indicator S p.1 * G.indicator T (G.nbr p.1 p.2) := + (Fintype.sum_prod_type + (fun p : G.V × G.D => G.indicator S p.1 * G.indicator T (G.nbr p.1 p.2))).symm + rw [hprod, dartsBetween, Finset.card_filter] + push_cast + refine Finset.sum_congr rfl fun p _ => ?_ + by_cases h1 : p.1 ∈ S + · by_cases h2 : G.nbr p.1 p.2 ∈ T <;> simp [indicator, h1, h2] + · simp [indicator, h1] + +/-- **The mixing lemma for vertex sets.** -/ +theorem mixing_sq_indicator {lam : ℝ} (h : G.SpectralBound lam) (hn : 0 < G.order) + (S T : Finset G.V) : + (((G.dartsBetween S T).card : ℝ) / (G.deg : ℝ) + - (S.card : ℝ) * (T.card : ℝ) / (G.order : ℝ)) ^ 2 + ≤ lam ^ 2 * ((S.card : ℝ) - (S.card : ℝ) ^ 2 / (G.order : ℝ)) + * ((T.card : ℝ) - (T.card : ℝ) ^ 2 / (G.order : ℝ)) := by + have hmix := G.mixing_sq h hn 1 (G.indicator S) (G.indicator T) + rw [stepIter_succ, stepIter_zero] at hmix + rwa [G.sum_indicator_mul_step S T, sum_indicator, sum_indicator, sum_sq_indicator, + sum_sq_indicator] at hmix + +/-- **Edge expansion.** A vertex set sends out at least +`(1 - lam) · deg · |S| · |Sᶜ| / n` darts. -/ +theorem card_dartsBetween_compl_ge {lam : ℝ} (hlam : 0 ≤ lam) (h : G.SpectralBound lam) + (hn : 0 < G.order) (S : Finset G.V) : + (1 - lam) * (G.deg : ℝ) * ((S.card : ℝ) * (Sᶜ.card : ℝ) / (G.order : ℝ)) + ≤ ((G.dartsBetween S Sᶜ).card : ℝ) := by + have hdq : (0 : ℝ) < (G.deg : ℝ) := by have := G.deg_pos; positivity + set B : ℝ := (S.card : ℝ) * (Sᶜ.card : ℝ) / (G.order : ℝ) with hB + -- both variances are `B` + have hcompl : (S.card : ℝ) + (Sᶜ.card : ℝ) = (G.order : ℝ) := by + have h : S.card + Sᶜ.card = Fintype.card G.V := Finset.card_add_card_compl S + rw [order] + exact_mod_cast h + have hvarS : (S.card : ℝ) - (S.card : ℝ) ^ 2 / (G.order : ℝ) = B := by + rw [hB] + field_simp + nlinarith [hcompl] + have hvarT : (Sᶜ.card : ℝ) - (Sᶜ.card : ℝ) ^ 2 / (G.order : ℝ) = B := by + rw [hB] + field_simp + nlinarith [hcompl] + have hmix := G.mixing_sq_indicator h hn S Sᶜ + rw [hvarS, hvarT] at hmix + -- the deviation is at most `lam * B` + set X : ℝ := ((G.dartsBetween S Sᶜ).card : ℝ) / (G.deg : ℝ) with hX + have hsq : (X - B) ^ 2 ≤ (lam * B) ^ 2 := by + calc (X - B) ^ 2 ≤ lam ^ 2 * B * B := by + rw [hB] at hmix ⊢ + exact hmix + _ = (lam * B) ^ 2 := by ring + have habs : B - X ≤ lam * B := by + have h1 : (B - X) ^ 2 ≤ (lam * B) ^ 2 := by + calc (B - X) ^ 2 = (X - B) ^ 2 := by ring + _ ≤ (lam * B) ^ 2 := hsq + exact le_of_sq_le_sq h1 (by positivity) + have hXge : (1 - lam) * B ≤ X := by linarith + calc (1 - lam) * (G.deg : ℝ) * B = (G.deg : ℝ) * ((1 - lam) * B) := by ring + _ ≤ (G.deg : ℝ) * X := by exact mul_le_mul_of_nonneg_left hXge (le_of_lt hdq) + _ = ((G.dartsBetween S Sᶜ).card : ℝ) := by rw [hX]; field_simp + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/Expander.lean b/Complexitylib/Classes/PCP/Internal/Expander.lean new file mode 100644 index 00000000..08474c4e --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/Expander.lean @@ -0,0 +1,140 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.Union +public import Complexitylib.Classes.PCP.Internal.NumEnc + +/-! +# Expander families, and expanderizing a graph + +Dinur's preprocessing needs a *fixed* constant degree `d` and a constant +`lam < 1` such that every vertex count admits a `d`-regular graph with spectral +bound `lam`. `ExpanderFamily` packages exactly that. + +The family is given as **raw rotation data** on `Fin n × Fin degree`, one +involution per vertex count, rather than as a function into `RegGraph`. That +matters downstream: degree reduction puts an expander on the cloud of *every* +vertex at once, and those clouds have different sizes, so all of them must speak +the same label type `Fin degree`. Deriving the graphs from shared data gives +that for free, and makes `order` and `deg` computations `Fintype.card_fin` +rather than hypotheses. + +Isolating the requirement this way matters for a second reason: nothing else in +the development depends on *how* the family is built. Neither Mathlib nor this +library currently contains an explicit expander construction — no spectral gap, +edge expansion, Cheeger inequality, or zig-zag product — so producing an +`ExpanderFamily` is a self-contained sub-project (zig-zag, or a Margulis-type +Cayley construction), and everything downstream is already stated against this +interface. + +## Main definitions + +- `RegGraph.ofRot` — a graph on `Fin n` from a rotation involution +- `ExpanderFamily` — constant degree, uniform spectral bound, shared label type +- `ExpanderFamily.graph`, `ExpanderFamily.expanderize` + +## Main results + +- `ExpanderFamily.order_graph`, `ExpanderFamily.deg_graph` +- `ExpanderFamily.spectralBound_expanderize` — expanderization has a bound +-/ + +@[expose] public section + +namespace Complexity + +namespace RegGraph + +/-- The graph on `Fin n` with labels `Fin d` given by a rotation involution. -/ +def ofRot (d : ℕ) (hd : 0 < d) (n : ℕ) (rot : Fin n × Fin d → Fin n × Fin d) + (hrot : Function.Involutive rot) : RegGraph where + V := Fin n + D := Fin d + decEqV := inferInstance + decEqD := inferInstance + fintypeV := inferInstance + fintypeD := inferInstance + nonemptyD := ⟨⟨0, hd⟩⟩ + rot := rot + rot_involutive := hrot + +@[simp] theorem order_ofRot (d : ℕ) (hd : 0 < d) (n : ℕ) + (rot : Fin n × Fin d → Fin n × Fin d) (hrot : Function.Involutive rot) : + (ofRot d hd n rot hrot).order = n := Fintype.card_fin n + +@[simp] theorem deg_ofRot (d : ℕ) (hd : 0 < d) (n : ℕ) + (rot : Fin n × Fin d → Fin n × Fin d) (hrot : Function.Involutive rot) : + (ofRot d hd n rot hrot).deg = d := Fintype.card_fin d + +end RegGraph + +/-- A family of constant-degree expanders, presented as rotation data: for every +vertex count `n`, an involution on `Fin n × Fin degree` whose graph contracts +mean-zero functions by a fixed `lam < 1`. -/ +structure ExpanderFamily where + /-- The constant degree, shared by every member. -/ + degree : ℕ + /-- The degree is positive. -/ + degree_pos : 0 < degree + /-- The rotation map on `n` vertices. -/ + rot : ∀ n : ℕ, Fin n × Fin degree → Fin n × Fin degree + /-- Each rotation map is an involution. -/ + rot_involutive : ∀ n, Function.Involutive (rot n) + /-- The uniform contraction factor. -/ + lam : ℝ + /-- The factor is nonnegative. -/ + lam_nonneg : 0 ≤ lam + /-- The factor is below one: this is the spectral gap. -/ + lam_lt_one : lam < 1 + /-- Every member contracts mean-zero functions by `lam`. -/ + spectral : ∀ n : ℕ, + (RegGraph.ofRot degree degree_pos n (rot n) (rot_involutive n)).SpectralBound lam + +namespace ExpanderFamily + +variable (E : ExpanderFamily) (G : RegGraph) [NumEnc G.V] + +/-- The member of the family on `n` vertices. -/ +def graph (n : ℕ) : RegGraph := + RegGraph.ofRot E.degree E.degree_pos n (E.rot n) (E.rot_involutive n) + +@[simp] theorem V_graph (n : ℕ) : (E.graph n).V = Fin n := rfl + +@[simp] theorem D_graph (n : ℕ) : (E.graph n).D = Fin E.degree := rfl + +@[simp] theorem order_graph (n : ℕ) : (E.graph n).order = n := Fintype.card_fin n + +@[simp] theorem deg_graph (n : ℕ) : (E.graph n).deg = E.degree := Fintype.card_fin E.degree + +theorem spectral_graph (n : ℕ) : (E.graph n).SpectralBound E.lam := E.spectral n + +/-- The identification of the family member's vertices with `G`'s: the +numbering `G`'s vertices carry. -/ +noncomputable def vertexEquiv : (E.graph G.order).V ≃ G.V := + (NumEnc.equivFinCard G.V).symm + +/-- `G` with a family expander superposed on its vertices. -/ +noncomputable def expanderize : RegGraph := + RegGraph.union G (E.graph G.order) (E.vertexEquiv G) + +@[simp] theorem order_expanderize : (E.expanderize G).order = G.order := rfl + +@[simp] theorem deg_expanderize : (E.expanderize G).deg = G.deg + E.degree := by + rw [expanderize, RegGraph.deg_union, deg_graph] + +/-- **Expanderization.** Superposing a family expander gives a graph with a +spectral bound. -/ +theorem spectralBound_expanderize : + (E.expanderize G).SpectralBound + (((G.deg : ℝ) + (E.degree : ℝ) * E.lam) / ((G.deg : ℝ) + (E.degree : ℝ))) := by + have h := RegGraph.spectralBound_union G (E.graph G.order) (E.vertexEquiv G) + E.lam_nonneg (E.spectral_graph G.order) + rw [deg_graph] at h + exact h + +end ExpanderFamily + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/ExpanderExists.lean b/Complexitylib/Classes/PCP/Internal/ExpanderExists.lean new file mode 100644 index 00000000..01f79936 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/ExpanderExists.lean @@ -0,0 +1,112 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.ExpanderRandom +public import Complexitylib.Classes.PCP.Internal.PermGraph +public import Complexitylib.Classes.PCP.Internal.Cheeger +public import Complexitylib.Classes.PCP.Internal.Expander + +/-! +# An expander family exists + +The three strands meet here. `ExpanderRandom` produces, for every `n`, thirty +permutations of `Fin n` no vertex set of at most half the vertices survives; +`PermGraph` turns those into a `60`-regular graph with edge expansion `1/600`; +and `Cheeger` converts edge expansion into a spectral gap once enough self-loops +are added to make the walk lazy. Relabelling the resulting `120` darts as +`Fin 120` puts the graph in the rotation-map form `ExpanderFamily` asks for. + +The construction is not explicit — the permutations come from +`Classical.choose` on a counting argument — which is all the mathematics of +Dinur's proof needs. An explicit family would be needed only to make the +reduction itself polynomial-time computable. + +## Main definitions + +- `Complexity.goodPerms` — the chosen permutations +- `Complexity.randExpander` — the resulting `ExpanderFamily` +-/ + +@[expose] public section + +namespace Complexity + +/-- Thirty permutations of `Fin n` that expand every small set. -/ +noncomputable def goodPerms (n : ℕ) : Fin 30 → Equiv.Perm (Fin n) := + Classical.choose (exists_good_perms n) + +theorem goodPerms_spec (n : ℕ) (S : Finset (Fin n)) (hS : 2 * S.card ≤ n) : + ∃ i, S.card ≤ 10 * escape (goodPerms n i) S := by + rcases S.eq_empty_or_nonempty with rfl | hne + · exact ⟨⟨0, by norm_num⟩, by simp⟩ + · exact Classical.choose_spec (exists_good_perms n) S hS hne + +/-- The `60`-regular graph of those permutations. -/ +noncomputable def baseGraph (n : ℕ) : RegGraph := + RegGraph.permsGraph (by norm_num : (0 : ℕ) < 30) (goodPerms n) + +theorem edgeExpansion_baseGraph (n : ℕ) : + (baseGraph n).EdgeExpansion (1 / (2 * (10 : ℝ) * 30)) := + RegGraph.edgeExpansion_permsGraph _ _ 10 (by norm_num) (goodPerms_spec n) + +theorem deg_baseGraph (n : ℕ) : (baseGraph n).deg = 60 := by + rw [baseGraph, RegGraph.deg_permsGraph] + +/-- The lazy walk on it: a self-loop for every dart. -/ +noncomputable def lazyGraph (n : ℕ) : RegGraph := (baseGraph n).padLoops (baseGraph n).deg + +/-- The spectral bound the construction achieves. -/ +noncomputable def randLam : ℝ := 1 - (1 / (2 * (10 : ℝ) * 30)) ^ 2 / 4 + +theorem randLam_nonneg : 0 ≤ randLam := by + rw [randLam] + norm_num + +theorem randLam_lt_one : randLam < 1 := by + rw [randLam] + norm_num + +theorem spectral_lazyGraph (n : ℕ) : (lazyGraph n).SpectralBound randLam := + RegGraph.spectralBound_padLoops_of_edgeExpansion _ (edgeExpansion_baseGraph n) (by norm_num) + +theorem deg_lazyGraph (n : ℕ) : (lazyGraph n).deg = 120 := by + rw [lazyGraph, RegGraph.deg_padLoops, deg_baseGraph] + +theorem card_lazyDarts (n : ℕ) : Fintype.card (lazyGraph n).D = 120 := deg_lazyGraph n + +/-- The darts of the lazy graph, named by `Fin 120`. -/ +noncomputable def dartEquiv (n : ℕ) : (lazyGraph n).D ≃ Fin 120 := + Fintype.equivFinOfCardEq (card_lazyDarts n) + +/-- The graph on `n` vertices with `Fin 120` darts. -/ +noncomputable def randGraph (n : ℕ) : RegGraph := (lazyGraph n).relabel (dartEquiv n) + +theorem spectral_randGraph (n : ℕ) : (randGraph n).SpectralBound randLam := + RegGraph.spectralBound_relabel _ _ (spectral_lazyGraph n) + +/-- Its rotation map, as data on `Fin n × Fin 120`. -/ +noncomputable def randRot (n : ℕ) : Fin n × Fin 120 → Fin n × Fin 120 := (randGraph n).rot + +theorem randRot_involutive (n : ℕ) : Function.Involutive (randRot n) := + (randGraph n).rot_involutive + +theorem ofRot_randRot (n : ℕ) : + RegGraph.ofRot 120 (by norm_num) n (randRot n) (randRot_involutive n) = randGraph n := rfl + +/-- **An expander family.** -/ +noncomputable def randExpander : ExpanderFamily where + degree := 120 + degree_pos := by norm_num + rot := randRot + rot_involutive := randRot_involutive + lam := randLam + lam_nonneg := randLam_nonneg + lam_lt_one := randLam_lt_one + spectral := fun n => by + rw [ofRot_randRot] + exact spectral_randGraph n + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/ExpanderMerge.lean b/Complexitylib/Classes/PCP/Internal/ExpanderMerge.lean new file mode 100644 index 00000000..accc8433 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/ExpanderMerge.lean @@ -0,0 +1,155 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.Expander +public import Complexitylib.Classes.PCP.Internal.Mixing + +/-! +# The pieces a merge is built from + +Explicit expander constructions come in special sizes — squares, powers — but +`ExpanderFamily` wants a member on *every* `n`. An expander on `N` vertices is +folded onto `n` of them by identifying `u` with `u mod n`, with self-loops +padding the fibres that come up short. `MergeGen` carries out that fold at an +arbitrary width; this module holds the two ingredients it rests on. + +The first is the estimate on the old steps. Write `f` for a function on the +merged graph and `F = f ∘ π` for its lift. Splitting `F` into its mean and its +centred part and applying the base's spectral bound to the latter gives +`λ² ‖F‖² + (1 - λ²) N c²`, where `c` is the mean of `F` — nonzero, because the +heavier fibres weigh more. + +The second is that the fibres stay *balanced*: with `(m - 1) n ≤ N` at most one +of a vertex's `m` slots is empty, so however large the width, the padding costs +one loop per vertex. + +## Main definitions + +- `Complexity.RegGraph.proj` — the new vertex an old one lands on +- `Complexity.RegGraph.liftN` — the old vertex in a given slot of a fibre + +## Main results + +- `Complexity.RegGraph.sum_sq_step_lift_le` — the old steps, with the mean + corrected +- `Complexity.RegGraph.card_liftN_none_le_one` — the balanced fibres a general + merge needs +-/ + +@[expose] public section + +namespace Complexity + +namespace RegGraph + +variable {N d n : ℕ} + +/-! ### The fibres of a merge -/ + +/-- The new vertex an old vertex lands on. -/ +def proj (n : ℕ) (hn : 0 < n) (u : Fin N) : Fin n := ⟨u.val % n, Nat.mod_lt _ hn⟩ + +/-! ### The spectral bound -/ + +section Spectral + +/-- The base graph, on `Fin N`. -/ +abbrev base (hd : 0 < d) (rot : Fin N × Fin d → Fin N × Fin d) + (hrot : Function.Involutive rot) : RegGraph := ofRot d hd N rot hrot + +/-- **The old steps of the lift**, with the mean corrected. -/ +theorem sum_sq_step_lift_le (hn : 0 < n) (hd : 0 < d) (rot : Fin N × Fin d → Fin N × Fin d) + (hrot : Function.Involutive rot) {lam : ℝ} (hspec : (base hd rot hrot).SpectralBound lam) + (hN0 : 0 < N) (f : Fin n → ℝ) : + ∑ u : Fin N, ((base hd rot hrot).step (fun w => f (proj n hn w)) u) ^ 2 + ≤ lam ^ 2 * ∑ u : Fin N, (f (proj n hn u)) ^ 2 + + (1 - lam ^ 2) * ((∑ u : Fin N, f (proj n hn u)) ^ 2 / (N : ℝ)) := by + classical + set G := base hd rot hrot with hG + set F : Fin N → ℝ := fun w => f (proj n hn w) with hF + have hord : (G.order : ℝ) = N := by rw [hG, order_ofRot] + have hordpos : 0 < G.order := by rw [hG, order_ofRot]; exact hN0 + -- decompose `F` into its mean and its centred part + have hdec : F = fun v => G.mean F + G.center F v := G.eq_mean_add_center F + have hstep : ∀ u, G.step F u = G.mean F + G.step (G.center F) u := by + intro u + conv_lhs => rw [hdec] + rw [G.step_add (fun _ => G.mean F) (G.center F) u, step_const] + have hcsum : ∑ u, G.center F u = 0 := G.sum_center hordpos F + have hstepsum : ∑ u, G.step (G.center F) u = 0 := by rw [G.sum_step, hcsum] + have hspec' := hspec (G.center F) hcsum + have hsq : ∑ u, (G.step F u) ^ 2 + = ∑ u, (G.step (G.center F) u) ^ 2 + (N : ℝ) * (G.mean F) ^ 2 := by + rw [Finset.sum_congr rfl fun u _ => by rw [hstep u]] + have : ∀ u, (G.mean F + G.step (G.center F) u) ^ 2 + = (G.step (G.center F) u) ^ 2 + 2 * G.mean F * G.step (G.center F) u + + (G.mean F) ^ 2 := fun u => by ring + have hcardV : Fintype.card (base hd rot hrot).V = N := Fintype.card_fin N + rw [Finset.sum_congr rfl fun u _ => this u, Finset.sum_add_distrib, + Finset.sum_add_distrib, ← Finset.mul_sum, hstepsum, Finset.sum_const, Finset.card_univ, + hcardV, nsmul_eq_mul] + ring + have hFsq := G.sum_sq_center hordpos F + rw [hord] at hFsq + have hmean : (N : ℝ) * (G.mean F) ^ 2 = (∑ u, F u) ^ 2 / (N : ℝ) := by + rw [mean, hord] + field_simp + rfl + show ∑ u, G.step F u ^ 2 ≤ lam ^ 2 * ∑ u, F u ^ 2 + (1 - lam ^ 2) * ((∑ u, F u) ^ 2 / (N : ℝ)) + rw [hsq, hmean] + have hsub : ∑ u, (G.center F u) ^ 2 = ∑ u, (F u) ^ 2 - (∑ u, F u) ^ 2 / (N : ℝ) := hFsq + rw [hsub] at hspec' + have hnn : 0 ≤ (∑ u, F u) ^ 2 / (N : ℝ) := by positivity + nlinarith [hspec', hnn] + +end Spectral + +/-! ### Balanced fibres, for a merge of any width + +With `(m - 1) n ≤ N ≤ m n` every fibre has `m - 1` or `m` elements, so at most +one of the `m` slots is empty and the padding costs one loop per vertex however +large `m` is. + +These are the two facts a general merge rests on; they are stated for a +natural-number slot index, which is the form the general construction needs. -/ + +/-- The old vertex in slot `i` of the fibre over `v`, if there is one. -/ +def liftN (N n : ℕ) (v : Fin n) (i : ℕ) : Option (Fin N) := + if h : v.val + i * n < N then some ⟨v.val + i * n, h⟩ else none + +/-- **Every slot but the last is filled**, when `(m - 1) n ≤ N`. -/ +theorem liftN_isSome {N n m : ℕ} (hm : (m - 1) * n ≤ N) (v : Fin n) {i : ℕ} + (hi : i + 1 < m) : (liftN N n v i).isSome := by + rw [liftN] + have hle : v.val + i * n < (m - 1) * n := by + have h1 : i + 1 ≤ m - 1 := by omega + calc v.val + i * n < n + i * n := by omega + _ = (i + 1) * n := by ring + _ ≤ (m - 1) * n := Nat.mul_le_mul_right _ h1 + rw [dif_pos (lt_of_lt_of_le hle hm)] + rfl + +/-- **So at most one slot is empty.** -/ +theorem card_liftN_none_le_one {N n m : ℕ} (hm : (m - 1) * n ≤ N) (v : Fin n) : + ((Finset.range m).filter fun i => liftN N n v i = none).card ≤ 1 := by + classical + refine Finset.card_le_one.2 fun i hi j hj => ?_ + rw [Finset.mem_filter, Finset.mem_range] at hi hj + by_contra hne + have hlast : ∀ k : ℕ, k < m → liftN N n v k = none → k + 1 = m := by + intro k hk hnone + by_contra hcon + have : k + 1 < m := by omega + have := liftN_isSome hm v this + rw [hnone] at this + exact absurd this (by simp) + have h1 := hlast i hi.1 hi.2 + have h2 := hlast j hj.1 hj.2 + omega + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/ExpanderPad.lean b/Complexitylib/Classes/PCP/Internal/ExpanderPad.lean new file mode 100644 index 00000000..7478badd --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/ExpanderPad.lean @@ -0,0 +1,260 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.Expander +public import Complexitylib.Classes.PCP.Internal.Mixing +public import Mathlib.Data.Real.Sqrt + +/-! +# Relabelling darts and padding with loops + +Two bookkeeping operations on regular graphs, both preserving the spectral +bound in an explicit way. + +* **Relabelling** the dart type along an equivalence. Constructions produce + structured dart types — pairs, functions, sums — while `ExpanderFamily` + wants `Fin degree`. The step operator is unchanged, so the bound is. + +* **Padding** with self-loops, to raise the degree of a graph to a prescribed + value. The new step is a convex combination of the old step and the identity, + so by Jensen the bound becomes `μ² = α λ² + (1 - α)` with `α` the fraction of + real darts. + +## Main definitions + +- `Complexity.RegGraph.relabel` — the same graph with darts renamed +- `Complexity.RegGraph.padLoops` — the graph with `k` self-loops added at every + vertex + +## Main results + +- `Complexity.RegGraph.spectralBound_relabel` +- `Complexity.RegGraph.spectralBound_padLoops` +- `Complexity.RegGraph.relabelV`, `Complexity.RegGraph.spectralBound_relabelV` — + renaming vertices +- `Complexity.RegGraph.toFinForm` — the same graph with both types numbered +-/ + +@[expose] public section + +namespace Complexity + +namespace RegGraph + +variable (G : RegGraph) + +/-! ### Relabelling -/ + +/-- The same graph with its darts renamed along `e`. -/ +def relabel {D' : Type} [DecidableEq D'] [Fintype D'] (e : G.D ≃ D') : RegGraph where + V := G.V + D := D' + decEqV := G.decEqV + decEqD := inferInstance + fintypeV := G.fintypeV + fintypeD := inferInstance + nonemptyD := ⟨e (Classical.arbitrary G.D)⟩ + rot := fun p => ((G.rot (p.1, e.symm p.2)).1, e (G.rot (p.1, e.symm p.2)).2) + rot_involutive := by + intro p + simp only [Equiv.symm_apply_apply] + have h := G.rot_involutive (p.1, e.symm p.2) + rw [show (G.rot (p.1, e.symm p.2)).1 = (G.rot (p.1, e.symm p.2)).1 from rfl] + conv_lhs => rw [show ((G.rot (p.1, e.symm p.2)).1, (G.rot (p.1, e.symm p.2)).2) + = G.rot (p.1, e.symm p.2) from rfl] + rw [h] + simp + +@[simp] theorem order_relabel {D' : Type} [DecidableEq D'] [Fintype D'] (e : G.D ≃ D') : + (G.relabel e).order = G.order := rfl + +theorem deg_relabel {D' : Type} [DecidableEq D'] [Fintype D'] (e : G.D ≃ D') : + (G.relabel e).deg = G.deg := by + show Fintype.card D' = Fintype.card G.D + exact (Fintype.card_congr e).symm + +theorem step_relabel {D' : Type} [DecidableEq D'] [Fintype D'] (e : G.D ≃ D') (f : G.V → ℝ) + (v : G.V) : (G.relabel e).step f v = G.step f v := by + simp only [step, deg_relabel] + congr 1 + show ∑ i : D', f (G.rot (v, e.symm i)).1 = ∑ i : G.D, f (G.rot (v, i)).1 + exact Fintype.sum_equiv e.symm _ _ fun i => rfl + +theorem spectralBound_relabel {D' : Type} [DecidableEq D'] [Fintype D'] (e : G.D ≃ D') + {lam : ℝ} (h : G.SpectralBound lam) : (G.relabel e).SpectralBound lam := by + intro f hf + have := h f hf + simp only [step_relabel] + exact this + +/-! ### Padding with loops -/ + +/-- The graph with `k` self-loops added at every vertex. -/ +def padLoops (k : ℕ) : RegGraph where + V := G.V + D := G.D ⊕ Fin k + decEqV := G.decEqV + decEqD := inferInstance + fintypeV := G.fintypeV + fintypeD := inferInstance + nonemptyD := ⟨Sum.inl (Classical.arbitrary G.D)⟩ + rot := fun p => + match p.2 with + | Sum.inl i => ((G.rot (p.1, i)).1, Sum.inl (G.rot (p.1, i)).2) + | Sum.inr j => (p.1, Sum.inr j) + rot_involutive := by + intro p + obtain ⟨v, i | j⟩ := p + · simp only + have h := G.rot_involutive (v, i) + conv_lhs => rw [show ((G.rot (v, i)).1, (G.rot (v, i)).2) = G.rot (v, i) from rfl] + rw [h] + · rfl + +@[simp] theorem order_padLoops (k : ℕ) : (G.padLoops k).order = G.order := rfl + +theorem deg_padLoops (k : ℕ) : (G.padLoops k).deg = G.deg + k := by + show Fintype.card (G.D ⊕ Fin k) = _ + rw [Fintype.card_sum, Fintype.card_fin] + rfl + +theorem step_padLoops (k : ℕ) (f : G.V → ℝ) (v : G.V) : + (G.padLoops k).step f v + = ((G.deg : ℝ) * G.step f v + (k : ℝ) * f v) / ((G.deg : ℝ) + k) := by + have hd : (G.deg : ℝ) ≠ 0 := G.deg_ne_zero + rw [step, deg_padLoops] + show (∑ i : G.D ⊕ Fin k, f ((G.padLoops k).nbr v i)) / ((G.deg + k : ℕ) : ℝ) = _ + rw [Fintype.sum_sum_type] + have h1 : ∀ i : G.D, (G.padLoops k).nbr v (Sum.inl i) = G.nbr v i := fun i => rfl + have h2 : ∀ j : Fin k, (G.padLoops k).nbr v (Sum.inr j) = v := fun j => rfl + simp only [h1, h2, Finset.sum_const, Finset.card_univ, Fintype.card_fin, nsmul_eq_mul] + push_cast + congr 1 + rw [step, mul_div_cancel₀ _ hd] + +/-- **Padding keeps the bound**, with `μ² = α λ² + (1 - α)` for `α` the fraction of +real darts. -/ +theorem spectralBound_padLoops (k : ℕ) {lam : ℝ} (h : G.SpectralBound lam) : + (G.padLoops k).SpectralBound + (Real.sqrt (((G.deg : ℝ) * lam ^ 2 + k) / ((G.deg : ℝ) + k))) := by + intro f hf + have hspec := h f hf + have hd : (0 : ℝ) < G.deg := by exact_mod_cast G.deg_pos + have hD : (0 : ℝ) < (G.deg : ℝ) + k := by positivity + rw [Real.sq_sqrt (by positivity)] + set a : ℝ := (G.deg : ℝ) / ((G.deg : ℝ) + k) with ha + have ha0 : 0 ≤ a := by positivity + have ha1 : a ≤ 1 := by rw [ha, div_le_one hD]; linarith + have hb : (k : ℝ) / ((G.deg : ℝ) + k) = 1 - a := by + rw [ha]; field_simp; ring + -- Jensen for two terms + have hpt : ∀ v, ((G.padLoops k).step f v) ^ 2 ≤ a * (G.step f v) ^ 2 + (1 - a) * (f v) ^ 2 := by + intro v + rw [step_padLoops] + have hrw : ((G.deg : ℝ) * G.step f v + (k : ℝ) * f v) / ((G.deg : ℝ) + k) + = a * G.step f v + (1 - a) * f v := by + rw [← hb, ha]; field_simp + rw [hrw] + nlinarith [mul_nonneg ha0 (sub_nonneg.2 ha1), sq_nonneg (G.step f v - f v)] + have hsum : ∑ v : G.V, ((G.padLoops k).step f v) ^ 2 + ≤ a * ∑ v : G.V, (G.step f v) ^ 2 + (1 - a) * ∑ v : G.V, (f v) ^ 2 := by + rw [Finset.mul_sum, Finset.mul_sum, ← Finset.sum_add_distrib] + exact Finset.sum_le_sum fun v _ => hpt v + have hcoef : ((G.deg : ℝ) * lam ^ 2 + k) / ((G.deg : ℝ) + k) = a * lam ^ 2 + (1 - a) := by + rw [← hb, ha]; field_simp + rw [hcoef] + show ∑ v : G.V, ((G.padLoops k).step f v) ^ 2 ≤ (a * lam ^ 2 + (1 - a)) * ∑ v : G.V, (f v) ^ 2 + nlinarith [hsum, mul_le_mul_of_nonneg_left hspec ha0] + +/-! ### Renaming vertices -/ + +/-- The same graph with its vertices renamed along `e`. Constructions produce +structured vertex types — products, sums — while a family wants `Fin n`. -/ +def relabelV {V' : Type} [DecidableEq V'] [Fintype V'] (e : G.V ≃ V') : RegGraph where + V := V' + D := G.D + decEqV := inferInstance + decEqD := G.decEqD + fintypeV := inferInstance + fintypeD := G.fintypeD + nonemptyD := G.nonemptyD + rot := fun p => (e (G.rot (e.symm p.1, p.2)).1, (G.rot (e.symm p.1, p.2)).2) + rot_involutive := by + intro p + obtain ⟨v, i⟩ := p + simp only [Equiv.symm_apply_apply] + have h := G.rot_involutive (e.symm v, i) + conv_lhs => rw [show ((G.rot (e.symm v, i)).1, (G.rot (e.symm v, i)).2) + = G.rot (e.symm v, i) from rfl] + rw [h] + simp + +@[simp] theorem deg_relabelV {V' : Type} [DecidableEq V'] [Fintype V'] (e : G.V ≃ V') : + (G.relabelV e).deg = G.deg := rfl + +theorem order_relabelV {V' : Type} [DecidableEq V'] [Fintype V'] (e : G.V ≃ V') : + (G.relabelV e).order = G.order := (Fintype.card_congr e).symm + +theorem step_relabelV {V' : Type} [DecidableEq V'] [Fintype V'] (e : G.V ≃ V') + (f : V' → ℝ) (v : V') : + (G.relabelV e).step f v = G.step (fun w => f (e w)) (e.symm v) := by + simp only [step, deg_relabelV] + congr 1 + +theorem spectralBound_relabelV {V' : Type} [DecidableEq V'] [Fintype V'] (e : G.V ≃ V') + {lam : ℝ} (h : G.SpectralBound lam) : (G.relabelV e).SpectralBound lam := by + intro f hf + have hf' : ∑ w : G.V, f (e w) = 0 := by + rw [Fintype.sum_equiv e (fun w => f (e w)) f fun w => rfl] + exact hf + have hb := h (fun w => f (e w)) hf' + calc ∑ v : V', ((G.relabelV e).step f v) ^ 2 + = ∑ w : G.V, (G.step (fun w => f (e w)) w) ^ 2 := + (Fintype.sum_equiv e (fun w => (G.step (fun w => f (e w)) w) ^ 2) + (fun v => ((G.relabelV e).step f v) ^ 2) fun w => by + dsimp only + rw [step_relabelV, Equiv.symm_apply_apply]).symm + _ ≤ lam ^ 2 * ∑ w : G.V, (f (e w)) ^ 2 := hb + _ = lam ^ 2 * ∑ v : V', (f v) ^ 2 := by + congr 1 + exact Fintype.sum_equiv e (fun w => (f (e w)) ^ 2) (fun v => (f v) ^ 2) fun w => rfl + +/-! ### Numbering both types -/ + +/-- The same graph with its vertices numbered `Fin order` and its darts +`Fin deg`. Constructions build structured types; the merge and the expander +families want numbered ones. -/ +noncomputable def toFinForm : RegGraph := + (G.relabelV (Fintype.equivFin G.V)).relabel (Fintype.equivFin G.D) + +@[simp] theorem order_toFinForm : G.toFinForm.order = G.order := by + rw [toFinForm, order_relabel, order_relabelV] + +@[simp] theorem deg_toFinForm : G.toFinForm.deg = G.deg := by + rw [toFinForm, deg_relabel, deg_relabelV] + +/-- The same graph numbered at sizes supplied by the caller, so that the +rotation map has the literal type `Fin N × Fin d → Fin N × Fin d` a numeric +construction expects, with no transport at the use site. -/ +noncomputable def toFinFormOf (N d : ℕ) (hN : Fintype.card G.V = N) + (hd : Fintype.card G.D = d) : RegGraph := + (G.relabelV ((Fintype.equivFin G.V).trans (finCongr hN))).relabel + ((Fintype.equivFin G.D).trans (finCongr hd)) + +@[simp] theorem order_toFinFormOf (N d : ℕ) (hN : Fintype.card G.V = N) + (hd : Fintype.card G.D = d) : (G.toFinFormOf N d hN hd).order = N := Fintype.card_fin N + +@[simp] theorem deg_toFinFormOf (N d : ℕ) (hN : Fintype.card G.V = N) + (hd : Fintype.card G.D = d) : (G.toFinFormOf N d hN hd).deg = d := Fintype.card_fin d + +theorem spectralBound_toFinFormOf (N d : ℕ) (hN : Fintype.card G.V = N) + (hd : Fintype.card G.D = d) {lam : ℝ} (h : G.SpectralBound lam) : + (G.toFinFormOf N d hN hd).SpectralBound lam := + spectralBound_relabel _ _ (spectralBound_relabelV G _ h) + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/ExpanderRandom.lean b/Complexitylib/Classes/PCP/Internal/ExpanderRandom.lean new file mode 100644 index 00000000..d6187367 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/ExpanderRandom.lean @@ -0,0 +1,216 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.PermArith +public import Complexitylib.Classes.PCP.Internal.PermCount + +/-! +# A tuple of permutations that expands + +The counting argument. Of the `(n!)^30` tuples of thirty permutations of +`Fin n`, not all can fail to expand: a tuple fails at a vertex set `S` of at +most half the vertices exactly when every one of its thirty permutations keeps +all but a tenth of `S` inside `S`. `PermCount` bounds how many permutations do +that for a fixed `S`; `PermArith.key_estimate` turns the thirtieth power of that +bound into `(n!)^30 / 2^{|S|}`, with room to spare for the `C(n,s)` sets of each +size; and summing `2^{-s}` over `s ≥ 1` stays below one. + +Everything is done with natural numbers — the geometric series appears as an +induction that carries the slack `+ K` explicitly, so no division is needed. + +## Main definitions + +- `Complexity.escLE` — the permutations keeping all but `t` points of `S` in `S` +- `Complexity.tOf` — the escape a set of a given size is allowed + +## Main results + +- `Complexity.exists_good_perms` — a tuple of thirty permutations for which + every set of at most half the vertices is moved out of itself, by at least a + tenth of it, by one of them +-/ + +@[expose] public section + +namespace Complexity + +open Finset + +variable {n : ℕ} + +/-- The permutations moving at most `t` points of `S` out of `S`. -/ +noncomputable def escLE (S : Finset (Fin n)) (t : ℕ) : Finset (Equiv.Perm (Fin n)) := + Finset.univ.filter fun σ => escape σ S ≤ t + +/-- The escape a set of size `s` is allowed before it counts as expanding. -/ +def tOf (S : Finset (Fin n)) : ℕ := (S.card - 1) / 10 + +theorem ten_mul_tOf_le (S : Finset (Fin n)) : 10 * tOf S ≤ S.card := by + rw [tOf] + omega + +theorem mem_escLE_iff {S : Finset (Fin n)} {σ : Equiv.Perm (Fin n)} (hS : 1 ≤ S.card) : + σ ∈ escLE S (tOf S) ↔ ¬ S.card ≤ 10 * escape σ S := by + simp only [escLE, Finset.mem_filter, Finset.mem_univ, true_and, tOf] + omega + +/-- The bound on how many permutations fail to expand a set of size `s`. -/ +def escB (n s : ℕ) : ℕ := + s.choose ((s - 1) / 10) * s.descFactorial (s - (s - 1) / 10) + * Nat.factorial (n - (s - (s - 1) / 10)) + +theorem card_escLE_le (S : Finset (Fin n)) : (escLE S (tOf S)).card ≤ escB n S.card := by + have hts : tOf S ≤ S.card := by have := ten_mul_tOf_le S; omega + have h := card_perm_escape_le S (tOf S) + rw [Nat.choose_symm hts] at h + rw [escLE, escB, ← tOf] + calc (Finset.univ.filter fun σ : Equiv.Perm (Fin n) => escape σ S ≤ tOf S).card + ≤ S.card.choose (tOf S) + * (S.card.descFactorial (S.card - tOf S) * Nat.factorial (n - (S.card - tOf S))) := h + _ = S.card.choose (tOf S) * S.card.descFactorial (S.card - tOf S) + * Nat.factorial (n - (S.card - tOf S)) := by ring + +/-- **The per-size estimate.** -/ +theorem two_pow_mul_escB_le {s : ℕ} (hs : 1 ≤ s) (hsn : 2 * s ≤ n) : + 2 ^ s * (n.choose s * escB n s ^ 30) ≤ Nat.factorial n ^ 30 := by + set t := (s - 1) / 10 with ht + set k := s - t with hk + have hts : t ≤ s := by omega + have h9 : 9 * s ≤ 10 * k := by omega + have hkn : k ≤ n := by omega + exact key_estimate hs hsn h9 (count_bound (by omega) hts hkn) + +/-! ### The geometric slack -/ + +private theorem sum_geom_bound {K : ℕ} : + ∀ (n : ℕ) (h : ℕ → ℕ), h 0 = 0 → (∀ s, 1 ≤ s → s ≤ n → 2 ^ s * h s ≤ K) → + 2 ^ n * ∑ s ∈ Finset.range (n + 1), h s + K ≤ 2 ^ n * K := by + intro n + induction n with + | zero => + intro h h0 _ + simp [h0] + | succ p ih => + intro h h0 hb + have hIH := ih h h0 fun s hs1 hsp => hb s hs1 (by omega) + have hlast : 2 ^ (p + 1) * h (p + 1) ≤ K := hb (p + 1) (by omega) le_rfl + rw [Finset.sum_range_succ] + set A := 2 ^ p with hA + set T0 := ∑ s ∈ Finset.range (p + 1), h s with hT0 + have hpow : (2 : ℕ) ^ (p + 1) = 2 * A := by rw [hA, pow_succ]; ring + rw [hpow] at hlast ⊢ + set x := A * T0 with hx + set y := A * h (p + 1) with hy + set z := A * K with hz + have e1 : 2 * A * (T0 + h (p + 1)) = 2 * x + 2 * y := by rw [hx, hy]; ring + have e2 : 2 * A * K = 2 * z := by rw [hz]; ring + have e3 : 2 * A * h (p + 1) = 2 * y := by rw [hy]; ring + rw [e1, e2] + rw [e3] at hlast + omega + +/-! ### The union bound -/ + +/-- **A tuple that expands.** -/ +theorem exists_good_perms (n : ℕ) : + ∃ σ : Fin 30 → Equiv.Perm (Fin n), ∀ S : Finset (Fin n), 2 * S.card ≤ n → + S.Nonempty → ∃ i, S.card ≤ 10 * escape (σ i) S := by + classical + set badSets : Finset (Finset (Fin n)) := + Finset.univ.filter fun S => 2 * S.card ≤ n ∧ S.Nonempty with hbadSets + set term : Finset (Fin n) → ℕ := fun S => (escLE S (tOf S)).card ^ 30 with hterm + set h : ℕ → ℕ := fun s => ∑ S ∈ badSets.filter fun S => S.card = s, term S with hh + -- the count of failing tuples + set BAD : Finset (Fin 30 → Equiv.Perm (Fin n)) := + Finset.univ.filter fun σ => ∃ S ∈ badSets, ∀ i, σ i ∈ escLE S (tOf S) with hBAD + have htotal : (Finset.univ : Finset (Fin 30 → Equiv.Perm (Fin n))).card + = Nat.factorial n ^ 30 := by + rw [Finset.card_univ, Fintype.card_pi] + simp [Fintype.card_perm] + have hbound : BAD.card ≤ ∑ S ∈ badSets, term S := by + have hsub : BAD ⊆ badSets.biUnion fun S => Fintype.piFinset fun _ => escLE S (tOf S) := by + intro σ hσ + rw [hBAD, Finset.mem_filter] at hσ + obtain ⟨S, hS, hall⟩ := hσ.2 + exact Finset.mem_biUnion.2 ⟨S, hS, Fintype.mem_piFinset.2 hall⟩ + refine le_trans (Finset.card_le_card hsub) (le_trans Finset.card_biUnion_le ?_) + refine le_of_eq (Finset.sum_congr rfl fun S _ => ?_) + rw [Fintype.card_piFinset, hterm] + simp + -- regroup by size + have hmaps : ∀ S ∈ badSets, S.card ∈ Finset.range (n + 1) := by + intro S hS + rw [hbadSets, Finset.mem_filter] at hS + exact Finset.mem_range.2 (by omega) + have hregroup : ∑ S ∈ badSets, term S = ∑ s ∈ Finset.range (n + 1), h s := + (Finset.sum_fiberwise_of_maps_to hmaps _).symm + -- the per-size bound + have hh0 : h 0 = 0 := by + rw [hh] + refine Finset.sum_eq_zero fun S hS => ?_ + exfalso + rw [Finset.mem_filter, hbadSets, Finset.mem_filter] at hS + have hempty : S = ∅ := Finset.card_eq_zero.1 hS.2 + exact absurd hS.1.2.2 (by rw [hempty]; exact Finset.not_nonempty_empty) + have hhb : ∀ s, 1 ≤ s → s ≤ n → 2 ^ s * h s ≤ Nat.factorial n ^ 30 := by + intro s hs1 _ + rcases Finset.eq_empty_or_nonempty (badSets.filter fun S => S.card = s) with he | ⟨S₀, hS₀⟩ + · rw [hh] + simp only + rw [he, Finset.sum_empty, Nat.mul_zero] + positivity + · have hS₀' := hS₀ + rw [Finset.mem_filter, hbadSets, Finset.mem_filter] at hS₀' + have hsn : 2 * s ≤ n := by rw [← hS₀'.2]; exact hS₀'.1.2.1 + have hcards : ∀ S ∈ badSets.filter fun S => S.card = s, term S ≤ escB n s ^ 30 := by + intro S hS + rw [Finset.mem_filter] at hS + have := card_escLE_le S + rw [hS.2] at this + rw [hterm] + exact Nat.pow_le_pow_left this 30 + have hcnt : (badSets.filter fun S => S.card = s).card ≤ n.choose s := by + have hsub : (badSets.filter fun S => S.card = s) + ⊆ Finset.powersetCard s Finset.univ := by + intro S hS + rw [Finset.mem_filter] at hS + exact Finset.mem_powersetCard.2 ⟨Finset.subset_univ _, hS.2⟩ + have := Finset.card_le_card hsub + rwa [Finset.card_powersetCard, Finset.card_univ, Fintype.card_fin] at this + have hsum : h s ≤ n.choose s * escB n s ^ 30 := by + rw [hh] + calc ∑ S ∈ badSets.filter fun S => S.card = s, term S + ≤ (badSets.filter fun S => S.card = s).card • escB n s ^ 30 := by + refine Finset.sum_le_card_nsmul _ _ _ fun S hS => ?_ + exact hcards S hS + _ = (badSets.filter fun S => S.card = s).card * escB n s ^ 30 := by + rw [smul_eq_mul] + _ ≤ n.choose s * escB n s ^ 30 := Nat.mul_le_mul_right _ hcnt + calc 2 ^ s * h s ≤ 2 ^ s * (n.choose s * escB n s ^ 30) := Nat.mul_le_mul_left _ hsum + _ ≤ Nat.factorial n ^ 30 := two_pow_mul_escB_le hs1 hsn + -- conclude + have hgeom := sum_geom_bound (K := Nat.factorial n ^ 30) n h hh0 hhb + have hfacpos : 0 < Nat.factorial n ^ 30 := pow_pos (Nat.factorial_pos n) 30 + have hlt : ∑ S ∈ badSets, term S < Nat.factorial n ^ 30 := by + rw [hregroup] + have h2 : (0 : ℕ) < 2 ^ n := pow_pos (by norm_num) n + nlinarith [hgeom, hfacpos, h2] + have hBADlt : BAD.card < (Finset.univ : Finset (Fin 30 → Equiv.Perm (Fin n))).card := by + rw [htotal] + exact lt_of_le_of_lt hbound hlt + obtain ⟨σ, -, hσ⟩ := Finset.exists_mem_notMem_of_card_lt_card hBADlt + refine ⟨σ, fun S hS hSne => ?_⟩ + by_contra hcon + push Not at hcon + refine hσ ?_ + rw [hBAD, Finset.mem_filter] + have hS1 : 1 ≤ S.card := Finset.card_pos.2 hSne + refine ⟨Finset.mem_univ _, S, ?_, + fun i => (mem_escLE_iff hS1).2 (by simpa using hcon i)⟩ + rw [hbadSets, Finset.mem_filter] + exact ⟨Finset.mem_univ _, hS, hSne⟩ + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/ExpanderizeCSP.lean b/Complexitylib/Classes/PCP/Internal/ExpanderizeCSP.lean new file mode 100644 index 00000000..a3e98638 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/ExpanderizeCSP.lean @@ -0,0 +1,171 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.Expander +public import Complexitylib.Classes.PCP.Internal.NumEnc +public import Complexitylib.Classes.PCP.Internal.RegCSP + +/-! +# Expanderizing a constraint system + +The second half of Dinur's preprocessing. Degree reduction makes the graph +regular; this step makes it an *expander*, by superposing a member of an +`ExpanderFamily` whose edges carry the trivially true constraint. + +Adding constraints that are never violated cannot create unsatisfiability, and +it cannot destroy it either: the broken darts are exactly the old ones, while +the total number of darts grows from `order · deg` to +`order · (deg + E.degree)`. So the value is scaled by exactly +`deg / (deg + E.degree)` — a constant factor, since both degrees are constants +after degree reduction — and satisfiability is unchanged. The spectral bound is +inherited from `spectralBound_union`. + +## Main definitions + +- `RegCSP.addTrivial` — superpose a graph's edges with trivial constraints +- `RegCSP.expanderize` — the case of a family expander + +## Main results + +- `RegCSP.card_unsatDarts_addTrivial` — the broken darts are unchanged +- `RegCSP.unsatFrac_addTrivial`, `unsatVal_addTrivial` — the value scales by + `deg / (deg + deg')` +- `RegCSP.satisfiable_addTrivial_iff` +- `RegCSP.spectralBound_expanderize` — the result is an expander +-/ + +@[expose] public section + +namespace Complexity + +namespace RegCSP + +variable {α : Type} (R : RegCSP α) (H : RegGraph) (e : H.V ≃ R.graph.V) + +/-- `R` with the edges of `H` superposed, carrying the trivially true +constraint. -/ +def addTrivial : RegCSP α where + graph := RegGraph.union R.graph H e + rel v d a b := + match d with + | Sum.inl i => R.rel v i a b + | Sum.inr _ => true + +@[simp] theorem graph_addTrivial : (R.addTrivial H e).graph = RegGraph.union R.graph H e := rfl + +/-- Only the original constraints can fail. -/ +theorem card_unsatDarts_addTrivial (a : R.Assignment) : + ((R.addTrivial H e).unsatDarts a).card = (R.unsatDarts a).card := by + classical + refine (Finset.card_bij (fun q _ => ((q.1, Sum.inl q.2) : (R.addTrivial H e).Dart)) ?_ ?_ ?_).symm + · intro q hq + rw [mem_unsatDarts] at hq ⊢ + intro hcon + exact hq hcon + · intro q _ q' _ heq + have h1 : q.1 = q'.1 := congrArg (fun r => (r.1 : R.graph.V)) heq + have h2 : Sum.inl q.2 = (Sum.inl q'.2 : R.graph.D ⊕ H.D) := + congrArg (fun r => (r.2 : R.graph.D ⊕ H.D)) heq + exact Prod.ext h1 (Sum.inl.inj h2) + · rintro ⟨v, i | j⟩ hq + · refine ⟨(v, i), ?_, rfl⟩ + rw [mem_unsatDarts] at hq ⊢ + intro hcon + exact hq hcon + · exfalso + rw [mem_unsatDarts] at hq + exact hq rfl + +/-- Superposing trivial constraints scales the value by `deg / (deg + deg')`. -/ +theorem unsatFrac_addTrivial (a : R.Assignment) : + (R.addTrivial H e).unsatFrac a + = R.unsatFrac a * (R.graph.deg : ℚ) / ((R.graph.deg : ℚ) + (H.deg : ℚ)) := by + have hd : (0 : ℚ) < (R.graph.deg : ℚ) := by + have := R.graph.deg_pos + exact_mod_cast this + have hcards := R.card_unsatDarts_addTrivial H e a + rcases Nat.eq_zero_or_pos R.graph.order with hz | hz + · have hempty : (R.unsatDarts a).card = 0 := by + have hle : (R.unsatDarts a).card ≤ R.graph.order * R.graph.deg := R.card_unsatDarts_le a + rw [hz] at hle + omega + have hempty' : ((R.addTrivial H e).unsatDarts a).card = 0 := by rw [hcards, hempty] + rw [unsatFrac, unsatFrac, hempty, hempty'] + simp + · have hzq : (0 : ℚ) < (R.graph.order : ℚ) := by exact_mod_cast hz + rw [unsatFrac, unsatFrac, hcards] + have hden : (((R.addTrivial H e).graph.order * (R.addTrivial H e).graph.deg : ℕ) : ℚ) + = (R.graph.order : ℚ) * ((R.graph.deg : ℚ) + (H.deg : ℚ)) := by + rw [graph_addTrivial, RegGraph.order_union, RegGraph.deg_union] + push_cast + ring + rw [hden] + field_simp + push_cast + ring + +/-- The scaling passes to the value, since it is the same factor for every +assignment and the assignments are the same. -/ +theorem unsatVal_addTrivial [Fintype α] [Nonempty α] : + (R.addTrivial H e).unsatVal + = R.unsatVal * (R.graph.deg : ℚ) / ((R.graph.deg : ℚ) + (H.deg : ℚ)) := by + have hk : (0 : ℚ) ≤ (R.graph.deg : ℚ) / ((R.graph.deg : ℚ) + (H.deg : ℚ)) := by positivity + obtain ⟨a, ha⟩ := R.exists_assignment_unsatFrac_eq_unsatVal + obtain ⟨b, hb⟩ := (R.addTrivial H e).exists_assignment_unsatFrac_eq_unsatVal + refine le_antisymm ?_ ?_ + · calc (R.addTrivial H e).unsatVal ≤ (R.addTrivial H e).unsatFrac a := + (R.addTrivial H e).unsatVal_le a + _ = R.unsatFrac a * (R.graph.deg : ℚ) / ((R.graph.deg : ℚ) + (H.deg : ℚ)) := + R.unsatFrac_addTrivial H e a + _ = R.unsatVal * (R.graph.deg : ℚ) / ((R.graph.deg : ℚ) + (H.deg : ℚ)) := by rw [ha] + · rw [← hb, R.unsatFrac_addTrivial H e b, mul_div_assoc, mul_div_assoc] + exact mul_le_mul_of_nonneg_right (R.unsatVal_le b) hk + +theorem satisfiable_addTrivial_iff : (R.addTrivial H e).Satisfiable ↔ R.Satisfiable := by + constructor + · rintro ⟨a, ha⟩ + refine ⟨a, fun p => ?_⟩ + have h := ha (p.1, Sum.inl p.2) + rw [Satisfies, satisfies] at h ⊢ + exact h + · rintro ⟨a, ha⟩ + refine ⟨a, ?_⟩ + rintro ⟨v, i | j⟩ + · have h := ha (v, i) + rw [Satisfies, satisfies] at h ⊢ + exact h + · rw [Satisfies, satisfies] + rfl + +/-- `R` with a family expander superposed. -/ +noncomputable def expanderize (R : RegCSP α) [NumEnc R.graph.V] (E : ExpanderFamily) : + RegCSP α := + R.addTrivial (E.graph R.graph.order) (E.vertexEquiv R.graph) + +@[simp] theorem graph_expanderize [NumEnc R.graph.V] (E : ExpanderFamily) : + (R.expanderize E).graph = E.expanderize R.graph := rfl + +/-- **The expanderized system is an expander.** -/ +theorem spectralBound_expanderize [NumEnc R.graph.V] (E : ExpanderFamily) : + (R.expanderize E).graph.SpectralBound + (((R.graph.deg : ℝ) + (E.degree : ℝ) * E.lam) + / ((R.graph.deg : ℝ) + (E.degree : ℝ))) := + E.spectralBound_expanderize R.graph + +theorem unsatVal_expanderize [Fintype α] [Nonempty α] [NumEnc R.graph.V] (E : ExpanderFamily) : + (R.expanderize E).unsatVal + = R.unsatVal * (R.graph.deg : ℚ) / ((R.graph.deg : ℚ) + (E.degree : ℚ)) := by + have h := R.unsatVal_addTrivial (E.graph R.graph.order) (E.vertexEquiv R.graph) + rw [E.deg_graph] at h + exact h + +theorem satisfiable_expanderize_iff [NumEnc R.graph.V] (E : ExpanderFamily) : + (R.expanderize E).Satisfiable ↔ R.Satisfiable := + R.satisfiable_addTrivial_iff _ _ + +end RegCSP + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/FamilyFin.lean b/Complexitylib/Classes/PCP/Internal/FamilyFin.lean new file mode 100644 index 00000000..f86e0b67 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/FamilyFin.lean @@ -0,0 +1,386 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.TowerFin +public import Complexitylib.Classes.PCP.Internal.MergeGen + +/-! +# An expander of every size, numbered + +`TowerFin` gives an expander at the tower's own sizes; a constraint graph asks +for one on exactly its own vertex count, and Dinur's degree reduction asks for +one on every vertex degree. This module closes the gap — fold the first large +enough member onto `n` vertices, then pad the degree with self-loops — keeping +every naming explicit, so that the rotation map stays a formula in numbers. + +Nothing is numbered by an arbitrary bijection: a slot and a dart are packed with +`finProdFinEquiv`, and the padding with `finSumFinEquiv`. That is what lets +`famRotVal` compute the family's rotation map on raw numerals, which is what an +algorithm has to be handed. + +## Main definitions + +- `Complexity.FinBase.famGraph` — the member on `n` vertices +- `Complexity.FinBase.toFamily` — the family + +## Main results + +- `Complexity.FinBase.spectral_famGraph` — its spectral bound, below one +-/ + +@[expose] public section + +namespace Complexity + +namespace FinBase + +variable (F : FinBase) (hd : 1 < F.deg) + +/-! ### The member folded onto `n` vertices -/ + +/-- The tower level used for `n` vertices: the first one at least twice as big. -/ +noncomputable def fitLevel (n : ℕ) : ℕ := F.level hd (2 * n) + +/-- **A fit's level is at most twice the size asked for**, so the polynomial an +algorithm needs to bound the search is linear. -/ +theorem fitLevel_le (n : ℕ) : F.fitLevel hd n ≤ 2 * n := F.level_le hd (2 * n) + +/-- Its size. -/ +noncomputable def fitN (n : ℕ) : ℕ := F.size (F.fitLevel hd n) + +/-- The degree of every tower member. -/ +def fitD : ℕ := F.deg ^ 2 + +theorem fitD_pos : 0 < F.fitD := F.sq_pos + +theorem two_mul_le_fitN (n : ℕ) : 2 * n ≤ F.fitN hd n := F.le_size_level hd (2 * n) + +/-- The width bound: the overshoot of the tower is at most `2 deg ^ 4`. -/ +def widthBnd : ℕ := 2 * F.deg ^ 4 + 1 + +/-- The family's degree. -/ +def famDeg : ℕ := F.widthBnd * F.fitD + +theorem famDeg_pos : 0 < F.famDeg := by + have h1 : 0 < F.widthBnd := by rw [widthBnd]; omega + exact Nat.mul_pos h1 F.fitD_pos + +theorem fitN_le {n : ℕ} (hn : 0 < n) : F.fitN hd n ≤ 2 * F.deg ^ 4 * n := by + have h := F.size_level_le hd (2 * n) (by omega) + rw [fitN, fitLevel] + calc F.size (F.level hd (2 * n)) ≤ F.deg ^ 4 * (2 * n) := h + _ = 2 * F.deg ^ 4 * n := by ring + +/-- The width of the fold. -/ +noncomputable def wid (n : ℕ) : ℕ := RegGraph.mergeWidth (F.fitN hd n) n + +theorem three_le_wid {n : ℕ} (hn : 0 < n) : 3 ≤ F.wid hd n := + RegGraph.three_le_mergeWidth hn (F.two_mul_le_fitN hd n) + +theorem wid_pos {n : ℕ} (hn : 0 < n) : 0 < F.wid hd n := by + have := F.three_le_wid hd hn + omega + +theorem wid_le {n : ℕ} (hn : 0 < n) : F.wid hd n ≤ F.widthBnd := + RegGraph.mergeWidth_le hn (F.fitN_le hd hn) + +theorem fitN_le_wid_mul {n : ℕ} (hn : 0 < n) : F.fitN hd n ≤ F.wid hd n * n := + RegGraph.le_mergeWidth_mul _ hn + +theorem wid_sub_one_mul_le (n : ℕ) : (F.wid hd n - 1) * n ≤ F.fitN hd n := + RegGraph.mergeWidth_sub_one_mul_le _ _ + +theorem wid_mul_fitD_le {n : ℕ} (hn : 0 < n) : F.wid hd n * F.fitD ≤ F.famDeg := + Nat.mul_le_mul_right _ (F.wid_le hd hn) + +/-- The tower member used for `n`, on its own numbers. -/ +noncomputable def fitGraph (n : ℕ) : RegGraph := F.graphAt (F.fitLevel hd n) + +@[simp] theorem order_fitGraph (n : ℕ) : (F.fitGraph hd n).order = F.fitN hd n := + F.order_graphAt _ + +@[simp] theorem deg_fitGraph (n : ℕ) : (F.fitGraph hd n).deg = F.fitD := + F.deg_graphAt _ + +theorem spectral_fitGraph (n : ℕ) : (F.fitGraph hd n).SpectralBound (2 / 5) := + spectral_graphAt F _ + +/-- Its rotation map, at the numeric type it lives on. -/ +noncomputable def fitRot (n : ℕ) : + Fin (F.fitN hd n) × Fin F.fitD → Fin (F.fitN hd n) × Fin F.fitD := + (F.data (F.fitLevel hd n)).1 + +theorem fitRot_involutive (n : ℕ) : Function.Involutive (F.fitRot hd n) := + (F.data (F.fitLevel hd n)).2 + +theorem base_fitRot (n : ℕ) : + RegGraph.base F.fitD_pos (F.fitRot hd n) (F.fitRot_involutive hd n) = F.fitGraph hd n := rfl + +/-! ### The fold, and the padding -/ + +/-- The tower member folded onto exactly `n` vertices. -/ +noncomputable def mergedG {n : ℕ} (hn : 0 < n) : RegGraph := + RegGraph.mergedN hn F.fitD_pos (F.wid_pos hd hn) (F.fitN_le_wid_mul hd hn) + (F.fitRot hd n) (F.fitRot_involutive hd n) + +@[simp] theorem order_mergedG {n : ℕ} (hn : 0 < n) : (F.mergedG hd hn).order = n := + RegGraph.order_mergedN _ _ _ _ _ _ + +@[simp] theorem deg_mergedG {n : ℕ} (hn : 0 < n) : + (F.mergedG hd hn).deg = F.wid hd n * F.fitD := + RegGraph.deg_mergedN _ _ _ _ _ _ + +theorem spectral_mergedG {n : ℕ} (hn : 0 < n) : (F.mergedG hd hn).SpectralBound (4 / 5) := by + have hspec : (RegGraph.base F.fitD_pos (F.fitRot hd n) + (F.fitRot_involutive hd n)).SpectralBound (2 / 5) := by + rw [F.base_fitRot hd n] + exact F.spectral_fitGraph hd n + have hmerged := RegGraph.spectralBound_mergedN hn F.fitD_pos (F.wid_pos hd hn) + (F.fitN_le_wid_mul hd hn) (F.wid_sub_one_mul_le hd n) (F.two_mul_le_fitN hd n) + (F.fitRot hd n) (F.fitRot_involutive hd n) (by norm_num) hspec + refine hmerged.mono (Real.sqrt_nonneg _) ?_ + rw [show (4 : ℝ) / 5 = Real.sqrt ((4 / 5) ^ 2) by rw [Real.sqrt_sq (by norm_num)]] + refine Real.sqrt_le_sqrt ?_ + have hm3 : (3 : ℝ) ≤ (F.wid hd n : ℝ) := by exact_mod_cast F.three_le_wid hd hn + set m : ℝ := (F.wid hd n : ℝ) + have h1 : (1 - (2 / 5 : ℝ) ^ 2) / (2 * m) ≤ (1 - (2 / 5 : ℝ) ^ 2) / (2 * 3) := + div_le_div_of_nonneg_left (by norm_num) (by norm_num) (by linarith) + have h2 : (1 : ℝ) / m ≤ 1 / 3 := + div_le_div_of_nonneg_left (by norm_num) (by norm_num) hm3 + nlinarith [h1, h2] + +/-- The fold padded up to the family's uniform degree. -/ +noncomputable def paddedG {n : ℕ} (hn : 0 < n) : RegGraph := + (F.mergedG hd hn).padLoops (F.famDeg - F.wid hd n * F.fitD) + +@[simp] theorem order_paddedG {n : ℕ} (hn : 0 < n) : (F.paddedG hd hn).order = n := + F.order_mergedG hd hn + +@[simp] theorem deg_paddedG {n : ℕ} (hn : 0 < n) : (F.paddedG hd hn).deg = F.famDeg := by + rw [paddedG, RegGraph.deg_padLoops, deg_mergedG] + have := F.wid_mul_fitD_le hd hn + omega + +/-- The uniform contraction factor of the family. -/ +noncomputable def famLam : ℝ := Real.sqrt (1 - 27 / (25 * F.widthBnd)) + +theorem three_le_widthBnd : 3 ≤ F.widthBnd := by + have h1 : 1 ≤ F.deg ^ 4 := Nat.one_le_pow _ _ F.deg_pos + rw [widthBnd] + omega + +theorem famLam_nonneg : 0 ≤ F.famLam := Real.sqrt_nonneg _ + +theorem famLam_lt_one : F.famLam < 1 := by + have hW : (3 : ℝ) ≤ (F.widthBnd : ℝ) := by exact_mod_cast F.three_le_widthBnd + have h0 : (0 : ℝ) ≤ 1 - 27 / (25 * F.widthBnd) := by + rw [sub_nonneg, div_le_one (by linarith)] + linarith + have hlt : (1 : ℝ) - 27 / (25 * F.widthBnd) < 1 := by + have : (0 : ℝ) < 27 / (25 * F.widthBnd) := by positivity + linarith + calc F.famLam = Real.sqrt (1 - 27 / (25 * F.widthBnd)) := rfl + _ < Real.sqrt 1 := Real.sqrt_lt_sqrt h0 hlt + _ = 1 := Real.sqrt_one + +theorem spectral_paddedG {n : ℕ} (hn : 0 < n) : (F.paddedG hd hn).SpectralBound F.famLam := by + have hpad := RegGraph.spectralBound_padLoops (G := F.mergedG hd hn) + (F.famDeg - F.wid hd n * F.fitD) (F.spectral_mergedG hd hn) + refine hpad.mono (Real.sqrt_nonneg _) ?_ + rw [famLam] + refine Real.sqrt_le_sqrt ?_ + have hle : F.wid hd n * F.fitD ≤ F.famDeg := F.wid_mul_fitD_le hd hn + have hF : (0 : ℝ) < (F.fitD : ℝ) := by exact_mod_cast F.fitD_pos + have hW : (3 : ℝ) ≤ (F.widthBnd : ℝ) := by exact_mod_cast F.three_le_widthBnd + have hm : (3 : ℝ) ≤ (F.wid hd n : ℝ) := by exact_mod_cast F.three_le_wid hd hn + have hDeg : ((F.famDeg : ℝ)) = (F.widthBnd : ℝ) * (F.fitD : ℝ) := by + rw [famDeg]; push_cast; ring + have hk : (((F.famDeg - F.wid hd n * F.fitD : ℕ) : ℝ)) + = (F.famDeg : ℝ) - (F.wid hd n : ℝ) * (F.fitD : ℝ) := by + rw [Nat.cast_sub hle]; push_cast; ring + have hDpos : (0 : ℝ) < (F.widthBnd : ℝ) * (F.fitD : ℝ) := by positivity + rw [F.deg_mergedG hd hn, hk] + push_cast + rw [hDeg, show ((F.wid hd n : ℝ) * (F.fitD : ℝ) + + ((F.widthBnd : ℝ) * (F.fitD : ℝ) - (F.wid hd n : ℝ) * (F.fitD : ℝ))) + = (F.widthBnd : ℝ) * (F.fitD : ℝ) from by ring, div_le_iff₀ hDpos, sub_mul, one_mul, + show 27 / (25 * (F.widthBnd : ℝ)) * ((F.widthBnd : ℝ) * (F.fitD : ℝ)) + = 27 * (F.fitD : ℝ) / 25 from by field_simp] + nlinarith [mul_le_mul_of_nonneg_right hm (le_of_lt hF)] + +/-! ### Numbering the darts -/ + +theorem wid_mul_fitD_add {n : ℕ} (hn : 0 < n) : + F.wid hd n * F.fitD + (F.famDeg - F.wid hd n * F.fitD) = F.famDeg := by + have := F.wid_mul_fitD_le hd hn + omega + +/-- The darts of the padded fold, numbered: a slot and a dart of the member are +packed together, and the padding loops follow them. -/ +noncomputable def famDartName {n : ℕ} (hn : 0 < n) : + (F.paddedG hd hn).D ≃ Fin F.famDeg := + (Equiv.sumCongr finProdFinEquiv (Equiv.refl _)).trans + (finSumFinEquiv.trans (finCongr (F.wid_mul_fitD_add hd hn))) + +/-- The member of the family on `n` vertices, for `n` positive. -/ +noncomputable def famGraph {n : ℕ} (hn : 0 < n) : RegGraph := + (F.paddedG hd hn).relabel (F.famDartName hd hn) + +theorem spectral_famGraph {n : ℕ} (hn : 0 < n) : + (F.famGraph hd hn).SpectralBound F.famLam := + RegGraph.spectralBound_relabel _ _ (F.spectral_paddedG hd hn) + +/-- The family's rotation map. -/ +noncomputable def famRot (n : ℕ) : Fin n × Fin F.famDeg → Fin n × Fin F.famDeg := + if hn : 0 < n then (F.famGraph hd hn).rot else id + +theorem famRot_involutive (n : ℕ) : Function.Involutive (F.famRot hd n) := by + rw [famRot] + split + · exact (F.famGraph hd ‹_›).rot_involutive + · exact fun x => rfl + +theorem famRot_eq {n : ℕ} (hn : 0 < n) : F.famRot hd n = (F.famGraph hd hn).rot := by + rw [famRot, dif_pos hn] + +theorem spectral_famRot (n : ℕ) : + (RegGraph.ofRot F.famDeg F.famDeg_pos n (F.famRot hd n) + (F.famRot_involutive hd n)).SpectralBound F.famLam := by + rcases Nat.eq_zero_or_pos n with h | hn + · subst h + exact RegGraph.spectralBound_of_isEmpty (by exact Fin.isEmpty') _ + · have key : ∀ (r : Fin n × Fin F.famDeg → Fin n × Fin F.famDeg) + (hr : Function.Involutive r), r = (F.famGraph hd hn).rot → + (RegGraph.ofRot F.famDeg F.famDeg_pos n r hr).SpectralBound F.famLam := by + rintro r hr rfl + exact F.spectral_famGraph hd hn + exact key _ _ (F.famRot_eq hd hn) + +/-! ### The rotation map, in numbers -/ + +theorem val_famDartName_inl {n : ℕ} (hn : 0 < n) (s : Fin (F.wid hd n)) (c : Fin F.fitD) : + ((F.famDartName hd hn) (Sum.inl (s, c) : (F.paddedG hd hn).D)).val + = c.val + F.fitD * s.val := rfl + +theorem famDartName_symm_of_lt {n : ℕ} (hn : 0 < n) (i : Fin F.famDeg) + (h : i.val < F.wid hd n * F.fitD) : + (F.famDartName hd hn).symm i + = Sum.inl (⟨i.val / F.fitD, by + exact (Nat.div_lt_iff_lt_mul F.fitD_pos).mpr h⟩, + ⟨i.val % F.fitD, Nat.mod_lt _ F.fitD_pos⟩) := by + rw [Equiv.symm_apply_eq] + refine Fin.ext ?_ + show i.val = i.val % F.fitD + F.fitD * (i.val / F.fitD) + exact (Nat.mod_add_div i.val F.fitD).symm + +theorem famDartName_symm_of_ge {n : ℕ} (hn : 0 < n) (i : Fin F.famDeg) + (h : F.wid hd n * F.fitD ≤ i.val) : + (F.famDartName hd hn).symm i + = (Sum.inr ⟨i.val - F.wid hd n * F.fitD, by + have := i.isLt + omega⟩ : (F.paddedG hd hn).D) := by + rw [Equiv.symm_apply_eq] + refine Fin.ext ?_ + show i.val = F.wid hd n * F.fitD + (i.val - F.wid hd n * F.fitD) + omega + +/-- **The family's rotation map, on raw numbers.** A dart below `wid * fitD` +splits into a slot and a dart of the tower member; the vertex it lifts to is +`v + slot * n`, and the vertex it lands on is read modulo `n`, with the slot it +landed in becoming part of the new dart. Every other dart is a self-loop. -/ +noncomputable def famRotVal (n : ℕ) (p : ℕ × ℕ) : ℕ × ℕ := + if p.2 < F.wid hd n * F.fitD then + if p.1 + p.2 / F.fitD * n < F.fitN hd n then + let y := F.rotVal (F.fitLevel hd n) (p.1 + p.2 / F.fitD * n, p.2 % F.fitD) + (y.1 % n, y.2 + F.fitD * (y.1 / n)) + else p + else p + +/-- **The numbers compute the family's rotation map.** -/ +theorem famRotVal_eq {n : ℕ} (hn : 0 < n) (v : Fin n) (i : Fin F.famDeg) : + F.famRotVal hd n (v.val, i.val) + = ((F.famRot hd n (v, i)).1.val, (F.famRot hd n (v, i)).2.val) := by + rw [F.famRot_eq hd hn] + by_cases hi : i.val < F.wid hd n * F.fitD + · obtain ⟨s, c, rfl⟩ : ∃ (s : Fin (F.wid hd n)) (c : Fin F.fitD), + i = F.famDartName hd hn (Sum.inl (s, c)) := by + refine ⟨⟨i.val / F.fitD, (Nat.div_lt_iff_lt_mul F.fitD_pos).mpr hi⟩, + ⟨i.val % F.fitD, Nat.mod_lt _ F.fitD_pos⟩, ?_⟩ + rw [← Equiv.symm_apply_eq] + exact F.famDartName_symm_of_lt hd hn i hi + simp only [famGraph, RegGraph.relabel, val_famDartName_inl] + rw [Equiv.symm_apply_apply] + show F.famRotVal hd n (v.val, c.val + F.fitD * s.val) + = (((F.mergedG hd hn).rot (v, (s, c))).1.val, + ((F.famDartName hd hn) + (Sum.inl ((F.mergedG hd hn).rot (v, (s, c))).2 : (F.paddedG hd hn).D)).val) + by_cases hu : v.val + s.val * n < F.fitN hd n + · have hlift : RegGraph.liftN (F.fitN hd n) n v s.val + = some (⟨v.val + s.val * n, hu⟩ : Fin (F.fitN hd n)) := by + rw [RegGraph.liftN, dif_pos hu] + simp only [mergedG, RegGraph.mergedN, RegGraph.mergeRotN, hlift] + have hlt : c.val + F.fitD * s.val < F.wid hd n * F.fitD := by + have h1 : c.val < F.fitD := c.isLt + have h2 : s.val + 1 ≤ F.wid hd n := s.isLt + nlinarith + have hs : (c.val + F.fitD * s.val) / F.fitD = s.val := by + rw [Nat.add_mul_div_left _ _ F.fitD_pos, Nat.div_eq_of_lt c.isLt, Nat.zero_add] + have hc : (c.val + F.fitD * s.val) % F.fitD = c.val := by + rw [Nat.add_mul_mod_self_left, Nat.mod_eq_of_lt c.isLt] + have hrot := F.rotVal_eq (F.fitLevel hd n) + (⟨v.val + s.val * n, hu⟩ : Fin (F.size (F.fitLevel hd n))) c + rw [famRotVal, if_pos hlt] + dsimp only + rw [hs, hc, if_pos hu, hrot] + rfl + · have hlift : RegGraph.liftN (F.fitN hd n) n v s.val = none := by + rw [RegGraph.liftN, dif_neg hu] + simp only [mergedG, RegGraph.mergedN, RegGraph.mergeRotN, hlift] + have hlt : c.val + F.fitD * s.val < F.wid hd n * F.fitD := by + have h1 : c.val < F.fitD := c.isLt + have h2 : s.val + 1 ≤ F.wid hd n := s.isLt + nlinarith + have hs : (c.val + F.fitD * s.val) / F.fitD = s.val := by + rw [Nat.add_mul_div_left _ _ F.fitD_pos, Nat.div_eq_of_lt c.isLt, Nat.zero_add] + rw [famRotVal, if_pos hlt] + dsimp only + rw [hs, if_neg hu] + rfl + · obtain ⟨j, rfl⟩ : ∃ j : Fin (F.famDeg - F.wid hd n * F.fitD), + i = F.famDartName hd hn (Sum.inr j : (F.paddedG hd hn).D) := by + refine ⟨⟨i.val - F.wid hd n * F.fitD, by have := i.isLt; omega⟩, ?_⟩ + rw [← Equiv.symm_apply_eq] + exact F.famDartName_symm_of_ge hd hn i (by omega) + simp only [famGraph, RegGraph.relabel] + rw [Equiv.symm_apply_apply] + have hge : ¬ (F.wid hd n * F.fitD + j.val < F.wid hd n * F.fitD) := by omega + show F.famRotVal hd n (v.val, F.wid hd n * F.fitD + j.val) = _ + rw [famRotVal, if_neg hge] + rfl + +/-- **The expander family the numbered tower generates**: one member at every +size, of a constant degree, all contracting by the same factor, and with every +naming explicit. -/ +noncomputable def toFamily : ExpanderFamily where + degree := F.famDeg + degree_pos := F.famDeg_pos + rot := F.famRot hd + rot_involutive := F.famRot_involutive hd + lam := F.famLam + lam_nonneg := F.famLam_nonneg + lam_lt_one := F.famLam_lt_one + spectral := F.spectral_famRot hd + +end FinBase + +/-! ### The family the algorithm uses -/ + +/-- A numbered base of degree above one, chosen once. -/ +noncomputable def algBase : FinBase := Classical.choose exists_finBase + +theorem one_lt_algBase_deg : 1 < algBase.deg := Classical.choose_spec exists_finBase + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/FiniteKey.lean b/Complexitylib/Classes/PCP/Internal/FiniteKey.lean new file mode 100644 index 00000000..4499fef4 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/FiniteKey.lean @@ -0,0 +1,91 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.FinsetDomain +public import Complexitylib.Classes.P.DecisionFn +public import Complexitylib.Classes.PCP.Internal.PCPtoSAT +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge + +/-! +# Decisions that depend on a bounded amount of data + +A constraint of a constraint graph looks at two symbols and a little local data, +and says yes or no. The rule may be described by something noncomputable — an +alphabet embedding chosen by `Classical.choice`, say — but it still runs in +polynomial time, because it is a table lookup on a bounded key. + +That is the content of this module: if a polynomial-time function extracts a key +of bounded length, then *any* predicate of that key is polynomial-time +decidable. + +The same argument gives functions, not just decisions: a value that depends on +the key alone is a table lookup too, whatever wrote the table. + +## Main results + +- `Complexity.mem_P_of_bounded_key` — a bounded-key predicate is in `P` +- `Complexity.mem_FP_of_bounded_key` — a bounded-key value is in `FP` +-/ + +@[expose] public section + +namespace Complexity + +open Classical in +/-- The strings of length at most `L` satisfying `P`. -/ +noncomputable def keySet (L : ℕ) (Q : List Bool → Prop) : Finset (List Bool) := + (Finset.range (L + 1)).biUnion fun n => + ((allVecs n).filter fun s => Q s).toFinset + +open Classical in +theorem mem_keySet {L : ℕ} {Q : List Bool → Prop} {s : List Bool} : + s ∈ keySet L Q ↔ s.length ≤ L ∧ Q s := by + classical + rw [keySet, Finset.mem_biUnion] + constructor + · rintro ⟨n, hn, hs⟩ + rw [List.mem_toFinset, List.mem_filter] at hs + rw [Finset.mem_range] at hn + have hlen := (mem_allVecs_iff n s).mp hs.1 + exact ⟨by omega, by simpa using hs.2⟩ + · rintro ⟨hlen, hQ⟩ + refine ⟨s.length, Finset.mem_range.mpr (by omega), ?_⟩ + rw [List.mem_toFinset, List.mem_filter] + exact ⟨(mem_allVecs_iff _ _).mpr rfl, by simpa using hQ⟩ + +/-- **A bounded-key predicate is in `P`.** The predicate itself need not be +computable; only the key extraction must be. -/ +theorem mem_P_of_bounded_key {key : List Bool → List Bool} (hkey : key ∈ FP) + {L : ℕ} (hL : ∀ z, (key z).length ≤ L) (Q : List Bool → Prop) : + {z : List Bool | Q (key z)} ∈ P := by + classical + have hite : (fun s => if s ∈ keySet L Q then [true] else ([] : List Bool)) ∈ FP := + ite_mem_finset_mem_FP (fun _ => [true]) (keySet L Q) + have hcomp : (fun z => if key z ∈ keySet L Q then [true] else ([] : List Bool)) ∈ FP := by + have := mem_FP_comp hkey hite + refine mem_FP_of_eq this fun z => ?_ + rw [Function.comp_apply] + refine mem_P_of_decisionFn hcomp fun z => ?_ + show Q (key z) ↔ _ + by_cases h : Q (key z) + · rw [if_pos (mem_keySet.mpr ⟨hL z, h⟩)] + exact ⟨fun _ => ⟨true, by simp, rfl⟩, fun _ => h⟩ + · rw [if_neg (fun hcon => h (mem_keySet.mp hcon).2)] + simp [h] + +/-- **A bounded-key value is in `FP`.** The rule computing the value from the +key need not be computable; only the key extraction must be. -/ +theorem mem_FP_of_bounded_key {key : List Bool → List Bool} (hkey : key ∈ FP) + {L : ℕ} (hL : ∀ z, (key z).length ≤ L) (g : List Bool → List Bool) : + (fun z => g (key z)) ∈ FP := by + classical + have hite : (fun s => if s ∈ keySet L (fun _ => True) then g s else ([] : List Bool)) ∈ FP := + ite_mem_finset_mem_FP g (keySet L (fun _ => True)) + have hcomp := mem_FP_comp hkey hite + refine mem_FP_of_eq hcomp fun z => ?_ + rw [Function.comp_apply, if_pos (mem_keySet.mpr ⟨hL z, trivial⟩)] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/FinsetPlurality.lean b/Complexitylib/Classes/PCP/Internal/FinsetPlurality.lean new file mode 100644 index 00000000..60b0741c --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/FinsetPlurality.lean @@ -0,0 +1,55 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Mathlib.Algebra.BigOperators.Fin +public import Mathlib.Data.Fintype.Card +public import Mathlib.Algebra.Order.BigOperators.Group.Finset +public import Mathlib.Tactic.Linarith + +/-! +# Plurality over a finite set + +The pigeonhole fact behind every "decode a cloud by majority vote" step: among +the `Fintype.card α` possible labels, some label is worn by at least a +`1 / Fintype.card α` fraction of a finite set. + +Dinur's degree reduction uses this to decode the blown-up assignment — a vertex +is given the label that the most half-edges of its cloud claim — and the bound +below is exactly what makes the *disagreeing* part of a cloud small enough for +the cloud expander to charge it. + +## Main results + +- `Complexity.exists_plurality` — some label captures at least `1 / card α` of + the set +-/ + +@[expose] public section + +namespace Complexity + +/-- **Plurality.** Some label is taken by at least a `1 / Fintype.card α` +fraction of `S`. -/ +theorem exists_plurality {β α : Type} [DecidableEq α] [Fintype α] [Nonempty α] + (S : Finset β) (A : β → α) : + ∃ a : α, S.card ≤ Fintype.card α * (S.filter fun p => A p = a).card := by + by_contra hcon + push Not at hcon + have hsum : ∑ a : α, (S.filter fun p => A p = a).card = S.card := + (Finset.card_eq_sum_card_fiberwise (fun x _ => Finset.mem_univ (A x))).symm + have hle : ∀ a : α, Fintype.card α * (S.filter fun p => A p = a).card + 1 ≤ S.card := + fun a => hcon a + have h1 : Fintype.card α * S.card + Fintype.card α ≤ Fintype.card α * S.card := by + calc Fintype.card α * S.card + Fintype.card α + = ∑ a : α, (Fintype.card α * (S.filter fun p => A p = a).card + 1) := by + rw [Finset.sum_add_distrib, ← Finset.mul_sum, hsum] + simp + _ ≤ ∑ _a : α, S.card := Finset.sum_le_sum fun a _ => hle a + _ = Fintype.card α * S.card := by simp [mul_comm] + have hpos : 0 < Fintype.card α := Fintype.card_pos + linarith + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/GapReduction.lean b/Complexitylib/Classes/PCP/Internal/GapReduction.lean new file mode 100644 index 00000000..ce3c2fd3 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/GapReduction.lean @@ -0,0 +1,68 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.Compose +public import Complexitylib.Classes.PCP.Internal.AlphabetLift +public import Complexitylib.Classes.PCP.Internal.ThreeSATReduction + +/-! +# The starting constraint graph, over the amplifier's alphabet + +Dinur's round is an endomorphism of constraint graphs over the alphabet the +composition step produces, `MultiTest.Alpha ReadIdx`, while the reduction from +3-SAT lands in `Fin 3 → Bool`. The latter has eight symbols and the former +`2^23`, so the small alphabet embeds, and `AlphabetLift` carries the graph +across without disturbing satisfiability. + +## Main definitions + +- `Complexity.alphaEmb` — an injection of the 3-SAT alphabet into the + amplifier's +- `Complexity.baseCSP` — the 3-SAT constraint graph, read over that alphabet + +## Main results + +- `Complexity.satisfiable_baseCSP_iff` — it is satisfiable exactly when the + formula is +-/ + +@[expose] public section + +namespace Complexity + +open ThreeSATCSP SAT + +/-- The alphabet Dinur's round runs over. -/ +abbrev GapAlpha : Type := MultiTest.Alpha ReadIdx + +theorem card_le_gapAlpha : Fintype.card (Fin 3 → Bool) ≤ Fintype.card GapAlpha := by + classical + have hl : Fintype.card (Fin 3 → Bool) = 8 := by + rw [Fintype.card_fun, Fintype.card_bool, Fintype.card_fin] + norm_num + have hr : Fintype.card GapAlpha = 2 * 2 ^ 22 := by + show Fintype.card (ZMod 2 × (ReadIdx → ZMod 2)) = 2 * 2 ^ 22 + rw [Fintype.card_prod, Fintype.card_fun, ZMod.card, card_readIdx] + rw [hl, hr] + norm_num + +/-- An injection of the 3-SAT alphabet into the amplifier's. -/ +noncomputable def alphaEmb : (Fin 3 → Bool) ↪ GapAlpha := + (Function.Embedding.nonempty_of_card_le card_le_gapAlpha).some + +/-- The 3-SAT constraint graph, read over the amplifier's alphabet. -/ +noncomputable def baseCSP (φ : CNF) : ConstraintGraph GapAlpha := + (toGraph φ).lift alphaEmb + +@[simp] theorem numEdges_baseCSP (φ : CNF) : (baseCSP φ).numEdges = 3 * φ.length := rfl + +theorem satisfiable_baseCSP_iff {φ : CNF} (h3 : φ.Is3CNF) : + (baseCSP φ).Satisfiable ↔ φ.Satisfiable := by + classical + rw [baseCSP, ConstraintGraph.satisfiable_lift_iff _ alphaEmb.injective] + exact satisfiable_toGraph_iff h3 + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/GuessVerifyGeneric.lean b/Complexitylib/Classes/PCP/Internal/GuessVerifyGeneric.lean new file mode 100644 index 00000000..b6af471a --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/GuessVerifyGeneric.lean @@ -0,0 +1,163 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P +public import Complexitylib.SAT.Internal.GuessVerify +public import Complexitylib.Classes.NP.Closure +public import Complexitylib.Classes.P.DecisionFn +public import Complexitylib.Classes.Containments +public import Complexitylib.Classes.P.Cobham.Internal.PolyRuler +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P.Cobham.Internal + +/-! +# Guess and verify, for any language + +The guess-and-verify machine built for SAT is not in fact specific to SAT: it +takes an arbitrary deterministic verifier `M`, guesses a string of length at +most `|x| + 1`, pairs it with the input and runs `M` on the result. Every +structural theorem about it in `SAT/Internal/GuessVerify` is already stated for +an arbitrary language `L`; only the final assembly mentions SAT. + +This module performs that assembly generically. The result is the +guess-and-verify bridge `NP.WitnessNTMConstruction` was meant to provide, +restricted to witnesses of linear length — which costs nothing, since padding +the input makes any polynomial witness bound linear. + +## Main results + +- `Complexity.mem_NP_of_linear_witness` — a language with a polynomial-time + verifier and witnesses of length at most `|x| + 1` is in `NP` +- `Complexity.mem_NP_of_poly_witness` — the same for any polynomial witness + bound, by padding the input until the bound is linear +-/ + +@[expose] public section + +namespace Complexity + +variable {k : ℕ} + +/-- The guess-and-verify machine decides any language whose members are exactly +the inputs with a short certificate accepted by `M`. -/ +theorem guessVerify_decidesInTime (M : TM k) {L L₀ : Language} {f : ℕ → ℕ} + (hM : M.DecidesInTime L₀ f) + (hchar : ∀ x, x ∈ L ↔ ∃ y : List Bool, y.length ≤ x.length + 1 ∧ pair x y ∈ L₀) : + (SAT.satGuessVerifyNTM M).DecidesInTime L (SAT.satGuessVerifyTime f) := by + refine ⟨SAT.satGuessVerify_allPathsHaltIn_of_decidesInTime M hM, ?_⟩ + intro x + constructor + · intro hx + obtain ⟨y, hlen, hmem⟩ := (hchar x).1 hx + exact SAT.satGuessVerify_acceptsInTime_of_witness_bound_of_decidesInTime M hM x y + hlen hmem + · intro hacc + by_contra hx + obtain ⟨choices, hhalt, hout⟩ := hacc + obtain ⟨y, hy, htrace⟩ := + SAT.satGuessVerify_trace_decides_for_some_setup_witness_of_decidesInTime M hM x choices + have hnot : pair x y ∉ L₀ := fun hmem => hx ((hchar x).2 ⟨y, hy, hmem⟩) + have hzero : ((SAT.satGuessVerifyNTM M).trace (SAT.satGuessVerifyTime f x.length) choices + ((SAT.satGuessVerifyNTM M).initCfg x)).output.cells 1 = Γ.zero := + htrace.2.2 hnot + rw [hzero] at hout + exact (by decide : Γ.zero ≠ Γ.one) hout + +/-- **Guess and verify.** A language whose members are exactly the inputs +carrying a certificate of length at most `|x| + 1` that a polynomial-time +verifier accepts is in `NP`. -/ +theorem mem_NP_of_linear_witness {L L₀ : Language} (hL₀ : L₀ ∈ P) + (hchar : ∀ x, x ∈ L ↔ ∃ y : List Bool, y.length ≤ x.length + 1 ∧ pair x y ∈ L₀) : + L ∈ NP := by + obtain ⟨c, k, M, f, hM, hfO⟩ := Set.mem_iUnion.mp hL₀ + obtain ⟨d, hgO⟩ := SAT.satGuessVerifyTime_bigO_of_bigO hfO + exact Set.mem_iUnion.mpr ⟨d, k + 3, SAT.satGuessVerifyNTM M, SAT.satGuessVerifyTime f, + guessVerify_decidesInTime M hM hchar, hgO⟩ + +/-! ### Any polynomial witness bound -/ + +/-- The input padded with a ruler long enough to make the witness bound linear. -/ +noncomputable def padWith (p : Polynomial ℕ) (x : List Bool) : List Bool := + pair x (polyRuler p x) + +theorem padWith_mem_FP (p : Polynomial ℕ) : padWith p ∈ FP := by + have h : (fun z : List Bool => polyRuler p (id z)) ∈ FP := polyRulerFn_mem_FP p id_mem_FP + exact Cobham.pairFn_mem_FP id_mem_FP h + +/-- The verifier for the padded language: run the original verifier on the +unpadded input, and check that the padding really is long enough. -/ +noncomputable def padVerifier (p : Polynomial ℕ) (L₀ : Language) : Language := + {w | pair (pairFst (pairFst w)) (pairSnd w) ∈ L₀ ∧ + (polyRuler p (pairFst (pairFst w))).length + ≤ (pairSnd (pairFst w)).length} + +theorem padVerifier_mem_P {p : Polynomial ℕ} {L₀ : Language} (hL₀ : L₀ ∈ P) : + padVerifier p L₀ ∈ P := by + have hff : (fun w : List Bool => pairFst (pairFst w)) ∈ FP := + fstBlockOf_mem_FP Cobham.fstBlock_mem_FP + have hsf : (fun w : List Bool => pairSnd (pairFst w)) ∈ FP := + sndBlockOf_mem_FP Cobham.fstBlock_mem_FP + have hA : (fun w : List Bool => + pair (pairFst (pairFst w)) (pairSnd w)) ⁻¹' L₀ ∈ P := + mem_P_preimage (Cobham.pairFn_mem_FP hff Cobham.sndBlock_mem_FP) hL₀ + have hruler : (fun w : List Bool => + polyRuler p (pairFst (pairFst w))) ∈ FP := + polyRulerFn_mem_FP p hff + have hB : {w : List Bool | + (polyRuler p (pairFst (pairFst w))).length + ≤ (pairSnd (pairFst w)).length} ∈ P := by + refine mem_P_of_decisionFn (lenLeFlagFn_mem_FP hsf hruler) fun w => ?_ + simp only [Set.mem_setOf_eq] + set a := pairSnd (pairFst w) with ha + set b := polyRuler p (pairFst (pairFst w)) with hb + constructor + · intro hle + rw [(Cobham.lenLeFlag_eq_true_iff a b).mpr hle] + exact ⟨true, by simp, rfl⟩ + · rintro ⟨c, hc, rfl⟩ + rcases Cobham.lenLeFlag_flag a b with h | h + · exact (Cobham.lenLeFlag_eq_true_iff a b).mp h + · rw [h] at hc + simp at hc + exact P_inter hA hB + +/-- The padded language, whose witnesses are short enough for the linear +guess-and-verify machine. -/ +noncomputable def padLang (p : Polynomial ℕ) (L₀ : Language) : Language := + {z | ∃ y : List Bool, y.length ≤ z.length + 1 ∧ pair z y ∈ padVerifier p L₀} + +theorem padLang_mem_NP {p : Polynomial ℕ} {L₀ : Language} (hL₀ : L₀ ∈ P) : + padLang p L₀ ∈ NP := + mem_NP_of_linear_witness (padVerifier_mem_P hL₀) fun _ => Iff.rfl + +/-- **Guess and verify, with any polynomial witness bound.** A language whose +members are exactly the inputs carrying a certificate a polynomial-time verifier +accepts is in `NP`, provided the verifier only accepts certificates of +polynomial length. -/ +theorem mem_NP_of_poly_witness {L L₀ : Language} (p : Polynomial ℕ) (hL₀ : L₀ ∈ P) + (hbal : ∀ x y : List Bool, pair x y ∈ L₀ → y.length ≤ p.eval x.length) + (hchar : ∀ x, x ∈ L ↔ ∃ y : List Bool, pair x y ∈ L₀) : + L ∈ NP := by + have hpre : L = padWith p ⁻¹' padLang p L₀ := by + ext x + rw [Set.mem_preimage, hchar x] + constructor + · rintro ⟨y, hy⟩ + refine ⟨y, ?_, ?_, ?_⟩ + · have := hbal x y hy + rw [padWith, pair_length, polyRuler_length] + omega + · rw [padWith, pairSnd_pair, pairFst_pair, pairFst_pair] + exact hy + · rw [padWith, pairFst_pair, pairFst_pair, pairSnd_pair] + · rintro ⟨y, _, hmem, _⟩ + refine ⟨y, ?_⟩ + rw [padWith, pairSnd_pair, pairFst_pair, pairFst_pair] at hmem + exact hmem + rw [hpre] + exact mem_NP_preimage (padWith_mem_FP p) (padLang_mem_NP hL₀) + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/Hadamard.lean b/Complexitylib/Classes/PCP/Internal/Hadamard.lean new file mode 100644 index 00000000..1b34df44 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/Hadamard.lean @@ -0,0 +1,535 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.BooleanAnalysis.FourierExpansion +public import Complexitylib.Classes.PCP.Internal.CubeBlocks + +/-! +# Decoding a Hadamard proof + +Dinur's alphabet-reduction step composes a constraint system with an inner +verifier — an *assignment tester* — and the classical one is built from the +Hadamard code: an assignment `a ⊆ Fin n` is encoded as the parity function +`χ a`, and the verifier checks that the proof is (close to) such a function. + +The two facts the analysis rests on are already in +`Complexitylib.BooleanAnalysis.FourierExpansion`: `blr_soundness`, which says a +proof passing the linearity test is close to *some* linear function, and +`local_correctability`, which recovers that function's value anywhere. What is +missing for a tester is the *witness*: soundness must hand back the coordinate +set `S`, since `S` is precisely the assignment being decoded. + +This module extracts that witness, and records the two identities that make +`χ S` a usable decoding: its value on a basis vector is the membership bit of +`S`, so reading the corrected proof at basis vectors recovers the assignment. + +## Main results + +- `Complexity.exists_close_parity_of_blr` — soundness with the coordinate set + named +- `Complexity.hadamard`, `Complexity.signOf_hadamard` — the Hadamard code is a + parity function, so the Fourier results apply to it +- `Complexity.tensor`, `Complexity.hadamard_tensor` — the consistency identity + tying the quadratic table to the linear one +- `Complexity.parity_eq_signOf_hadamard`, `Complexity.exists_assignment_of_blr` — + a proof passing the linearity test decodes to an assignment +- `Complexity.eq_tensorAssign_of_consistent` — the consistency check leaves the + prover no freedom in the quadratic table +- `Complexity.hammingDist_comm`, `Complexity.hammingDist_triangle` — basic + metric facts, absent from the Fourier layer +- `Complexity.prob_hadamard_ne_zero` — a nonzero linear form is balanced +- `Complexity.tensorRow`, `Complexity.hadamard_tensor_row` — the bilinear form, + read as a linear form in one argument +- `Complexity.prob_mono`, `Complexity.prob_tensorRow_ne_zero` — a nonzero + bilinear form has nonzero rows for at least half the second arguments +- `Complexity.prob₂_tensor_ne_zero` — hence it is nonzero on a quarter of all + pairs +- `Complexity.eq_tensorAssign_of_prob_consistent` — so passing the consistency + check often forces the quadratic table exactly +-/ + +@[expose] public section + +namespace Complexity + +open BooleanAnalysis + +variable {n : ℕ} + +/-- **BLR soundness, with the witness named.** A proof passing the linearity +test is close to `χ S` for an explicit coordinate set `S` — and `S` is the +assignment the tester decodes. -/ +theorem exists_close_parity_of_blr (f : BooleanFunction n) (hf : IsBooleanValued f) + (ε : ℝ) (h : blrAcceptProb f ≥ 1 - ε) : + ∃ S : Finset (Fin n), hammingDist f (χ S) ≤ ε := by + obtain ⟨g, ⟨S, hg⟩, hclose⟩ := blr_soundness f hf ε h + refine ⟨S, ?_⟩ + have hgS : g = χ S := funext hg + rw [← hgS] + exact hclose + +/-- The basis vector at coordinate `i`. -/ +def basisVec (i : Fin n) : Cube n := fun j => if j = i then 1 else 0 + +/-! ### The Hadamard code -/ + +/-- An `𝔽₂`-valued function on the cube. The tester's checks are stated over +`𝔽₂` — the consistency check multiplies two bits, which is not a `±1` +operation — while the linearity analysis lives in the `±1` world, so the two +views must be bridged. -/ +abbrev BitFun (n : ℕ) : Type := Cube n → ZMod 2 + +/-- The `±1` encoding of an `𝔽₂`-valued function. -/ +noncomputable def signOf (F : BitFun n) : BooleanFunction n := fun x => chi (F x) + +theorem chi_sum {ι : Type*} (s : Finset ι) (f : ι → ZMod 2) : + chi (∑ i ∈ s, f i) = ∏ i ∈ s, chi (f i) := by + classical + induction s using Finset.induction with + | empty => simp [chi] + | insert a s ha ih => + rw [Finset.sum_insert ha, Finset.prod_insert ha, BooleanAnalysis.Internal.chi_add, ih] + +/-- The Hadamard encoding of an assignment `a`: the linear function +`x ↦ ⟨a, x⟩`. -/ +def hadamard (a : Cube n) : BitFun n := fun x => ∑ i, a i * x i + +/-- **The Hadamard code is a parity function.** Its `±1` encoding is `χ` of the +support of the assignment — so the Fourier layer's linearity results apply to +Hadamard proofs verbatim, and the decoded coordinate set is the assignment's +support. -/ +theorem signOf_hadamard (a : Cube n) : + signOf (hadamard a) = χ (Finset.univ.filter fun i => a i = 1) := by + classical + funext x + show chi (∑ i, a i * x i) = ∏ i ∈ Finset.univ.filter (fun i => a i = 1), chi (x i) + rw [chi_sum] + rw [← Finset.prod_filter_mul_prod_filter_not Finset.univ (fun i => a i = 1) + (fun i => chi (a i * x i))] + have hone : ∏ i ∈ Finset.univ.filter (fun i => ¬ a i = 1), chi (a i * x i) = 1 := by + refine Finset.prod_eq_one fun i hi => ?_ + simp only [Finset.mem_filter] at hi + have hzero : a i = 0 := by + rcases (by decide : ∀ b : ZMod 2, b = 0 ∨ b = 1) (a i) with h | h + · exact h + · exact absurd h hi.2 + rw [hzero, zero_mul] + simp + rw [hone, mul_one] + refine Finset.prod_congr rfl fun i hi => ?_ + simp only [Finset.mem_filter] at hi + rw [hi.2, one_mul] + +/-! ### The tensor part -/ + +/-- The outer product of two cube points, as a point of the squared cube. The +Fourier layer is indexed by `Fin n`, so the pair index is transported along +`finProdFinEquiv`. -/ +def tensor (x y : Cube n) : Cube (n * n) := + fun k => x (finProdFinEquiv.symm k).1 * y (finProdFinEquiv.symm k).2 + +/-- The tensor square of an assignment — the second table a Hadamard proof +carries, so that the verifier can evaluate quadratic constraints. -/ +def tensorAssign (a : Cube n) : Cube (n * n) := tensor a a + +/-- **The consistency identity.** The tensor table, read at an outer product, +is the product of the two linear readings. This is the check that ties the +quadratic table to the linear one, and it is a statement about *bits*: over +`𝔽₂` the right-hand side is a product, which is why the tester's checks cannot +be phrased in the `±1` encoding. -/ +theorem hadamard_tensor (a x y : Cube n) : + hadamard (tensorAssign a) (tensor x y) = hadamard a x * hadamard a y := by + classical + have hre : ∀ f : Fin n × Fin n → ZMod 2, + ∑ k : Fin (n * n), f (finProdFinEquiv.symm k) = ∑ p : Fin n × Fin n, f p := by + intro f + exact Fintype.sum_equiv finProdFinEquiv.symm (fun k => f (finProdFinEquiv.symm k)) f + fun k => rfl + show ∑ k : Fin (n * n), (tensorAssign a) k * (tensor x y) k + = (∑ i, a i * x i) * (∑ j, a j * y j) + have hstep : ∑ k : Fin (n * n), (tensorAssign a) k * (tensor x y) k + = ∑ p : Fin n × Fin n, (a p.1 * a p.2) * (x p.1 * y p.2) := + hre fun p => (a p.1 * a p.2) * (x p.1 * y p.2) + rw [hstep, Fintype.sum_prod_type, Finset.sum_mul_sum] + refine Finset.sum_congr rfl fun i _ => Finset.sum_congr rfl fun j _ => ?_ + ring + +/-! ### Decoding a proof into an assignment -/ + +/-- The assignment a coordinate set stands for. -/ +def indicatorAssign (S : Finset (Fin n)) : Cube n := fun i => if i ∈ S then 1 else 0 + +/-- **Every parity function is a Hadamard codeword.** With `signOf_hadamard` +this makes the correspondence between assignments and parity functions a +bijection, so BLR's coordinate set can be handed back as an assignment. -/ +theorem parity_eq_signOf_hadamard (S : Finset (Fin n)) : + (χ S) = signOf (hadamard (indicatorAssign S)) := by + classical + rw [signOf_hadamard] + congr 1 + ext i + simp only [Finset.mem_filter, Finset.mem_univ, true_and, indicatorAssign] + by_cases hi : i ∈ S + · simp [hi] + · simp [hi] + +/-- **Decoding.** A proof passing the linearity test is close to the Hadamard +encoding of an explicit assignment — the assignment the tester extracts. -/ +theorem exists_assignment_of_blr (f : BooleanFunction n) (hf : IsBooleanValued f) + (ε : ℝ) (h : blrAcceptProb f ≥ 1 - ε) : + ∃ a : Cube n, hammingDist f (signOf (hadamard a)) ≤ ε := by + obtain ⟨S, hS⟩ := exists_close_parity_of_blr f hf ε h + refine ⟨indicatorAssign S, ?_⟩ + rw [← parity_eq_signOf_hadamard S] + exact hS + +/-! ### Consistency forces the tensor -/ + +/-- Reading a Hadamard table at a basis vector returns that coordinate. -/ +theorem hadamard_basisVec (a : Cube n) (i : Fin n) : hadamard a (basisVec i) = a i := by + classical + show ∑ j, a j * (basisVec i) j = a i + rw [Finset.sum_eq_single i] + · show a i * (if i = i then (1 : ZMod 2) else 0) = a i + rw [if_pos rfl, mul_one] + · intro j _ hj + show a j * (if j = i then (1 : ZMod 2) else 0) = 0 + rw [if_neg hj, mul_zero] + · intro hni + exact absurd (Finset.mem_univ i) hni + +/-- The outer product of two basis vectors is the basis vector at the +corresponding pair index. -/ +theorem tensor_basisVec (i j : Fin n) : + tensor (basisVec i) (basisVec j) = basisVec (finProdFinEquiv (i, j)) := by + classical + funext k + show (basisVec i) (finProdFinEquiv.symm k).1 * (basisVec j) (finProdFinEquiv.symm k).2 + = if k = finProdFinEquiv (i, j) then (1 : ZMod 2) else 0 + by_cases hk : k = finProdFinEquiv (i, j) + · subst hk + rw [if_pos rfl, Equiv.symm_apply_apply] + show (if i = i then (1 : ZMod 2) else 0) * (if j = j then (1 : ZMod 2) else 0) = 1 + rw [if_pos rfl, if_pos rfl, mul_one] + · rw [if_neg hk] + by_cases h1 : (finProdFinEquiv.symm k).1 = i + · by_cases h2 : (finProdFinEquiv.symm k).2 = j + · exfalso + apply hk + have hpair : finProdFinEquiv.symm k = (i, j) := Prod.ext h1 h2 + rw [← hpair, Equiv.apply_symm_apply] + · show (if (finProdFinEquiv.symm k).1 = i then (1 : ZMod 2) else 0) + * (if (finProdFinEquiv.symm k).2 = j then (1 : ZMod 2) else 0) = 0 + rw [if_neg h2, mul_zero] + · show (if (finProdFinEquiv.symm k).1 = i then (1 : ZMod 2) else 0) + * (if (finProdFinEquiv.symm k).2 = j then (1 : ZMod 2) else 0) = 0 + rw [if_neg h1, zero_mul] + +/-- **Consistency forces the tensor.** A quadratic table that agrees with the +product of the linear readings on every outer product *is* the tensor square of +the assignment — testing at basis vectors pins down every entry. This is why the +consistency check suffices: it leaves the prover no freedom in the quadratic +table. -/ +theorem eq_tensorAssign_of_consistent (a : Cube n) (b : Cube (n * n)) + (h : ∀ x y : Cube n, hadamard b (tensor x y) = hadamard a x * hadamard a y) : + b = tensorAssign a := by + classical + funext k + set i := (finProdFinEquiv.symm k).1 with hi + set j := (finProdFinEquiv.symm k).2 with hj + have hk : k = finProdFinEquiv (i, j) := by + rw [hi, hj, Prod.mk.eta, Equiv.apply_symm_apply] + have hb := h (basisVec i) (basisVec j) + rw [tensor_basisVec, hadamard_basisVec, hadamard_basisVec, hadamard_basisVec] at hb + show b k = a i * a j + rw [hk] + exact hb + +/-! ### Checking a constraint -/ + +/-- A quadratic constraint over `𝔽₂`. Dinur's inner verifier tests exactly +this: the constraints of a system with a constant-size alphabet are quadratic +equations once the alphabet symbols are spelled out in bits. -/ +structure QuadConstraint (n : ℕ) where + /-- The quadratic coefficients, indexed like the tensor table. -/ + quad : Cube (n * n) + /-- The linear coefficients. -/ + lin : Cube n + /-- The constant term. -/ + const : ZMod 2 + +/-- The constraint evaluated at an assignment. -/ +def QuadConstraint.eval (C : QuadConstraint n) (a : Cube n) : ZMod 2 := + hadamard (tensorAssign a) C.quad + hadamard a C.lin + C.const + +/-- The assignment satisfies the constraint. -/ +def QuadConstraint.Sat (C : QuadConstraint n) (a : Cube n) : Prop := C.eval a = 0 + +/-- What the verifier computes from the two tables: one query into each. -/ +def checkValue (F : BitFun (n * n)) (f : BitFun n) (C : QuadConstraint n) : ZMod 2 := + F C.quad + f C.lin + C.const + +/-! ### Rigidity of the decoding -/ + +theorem hammingDist_comm (f g : BooleanFunction n) : + hammingDist f g = hammingDist g f := by + have hpred : (fun x => f x ≠ g x) = (fun x => g x ≠ f x) := by + funext x + exact propext ⟨fun h => Ne.symm h, fun h => Ne.symm h⟩ + rw [hammingDist, hammingDist, hpred] + +theorem hammingDist_triangle (f g h : BooleanFunction n) : + hammingDist f h ≤ hammingDist f g + hammingDist g h := by + have hbound := BooleanAnalysis.Internal.prob_union_bound + (P := fun x => f x = h x) (Q := fun x => f x ≠ g x) (R := fun x => g x ≠ h x) + (fun x hx => by + by_contra hcon + push Not at hcon + exact hx (hcon.1.trans hcon.2)) + exact hbound + +/-! ### A nonzero linear form is balanced -/ + +/-- The `±1` encoding turns a bit into `1 - 2·bit`. -/ +theorem chi_eq_one_sub_two (v : ZMod 2) : + chi v = 1 - 2 * (if v ≠ 0 then (1 : ℝ) else 0) := by + by_cases h : v = 0 + · rw [h] + norm_num [chi] + · rw [if_pos h] + have hchi : chi v = -1 := by simp [chi, h] + rw [hchi] + norm_num + +theorem card_cube (n : ℕ) : Fintype.card (Cube n) = 2 ^ n := by + show Fintype.card (Fin n → ZMod 2) = 2 ^ n + rw [Fintype.card_fun, ZMod.card, Fintype.card_fin] + +/-- The expectation of a bit function's sign encoding. -/ +theorem expect_signOf (F : BitFun n) : + 𝔼[signOf F] = 1 - 2 * Pr[fun x => F x ≠ 0] := by + classical + rw [expect_unfold, BooleanAnalysis.prob, expect_unfold] + simp only [BooleanAnalysis.indicator] + have hterm : ∀ x : Cube n, (signOf F) x = 1 - 2 * (if F x ≠ 0 then (1 : ℝ) else 0) := + fun x => chi_eq_one_sub_two (F x) + rw [Finset.sum_congr rfl fun x _ => hterm x, Finset.sum_sub_distrib, ← Finset.mul_sum] + have hcard : ∑ _x : Cube n, (1 : ℝ) = 2 ^ n := by + rw [Finset.sum_const, Finset.card_univ, card_cube, nsmul_eq_mul, mul_one] + norm_num + rw [hcard] + field_simp + ring_nf + +/-- **A nonzero linear form is balanced.** Over `𝔽₂` a nonzero linear form takes +each value on exactly half the cube — the counting fact behind every "the check +cannot pass too often unless it always passes" step. -/ +theorem prob_hadamard_ne_zero (a : Cube n) (ha : a ≠ 0) : + Pr[fun x => hadamard a x ≠ 0] = 1 / 2 := by + classical + have hne : (Finset.univ.filter fun i => a i = 1) ≠ ∅ := by + intro hempty + apply ha + funext i + have hi : i ∉ Finset.univ.filter fun j => a j = 1 := by + rw [hempty] + exact Finset.notMem_empty i + simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hi + rcases (by decide : ∀ b : ZMod 2, b = 0 ∨ b = 1) (a i) with h | h + · exact h + · exact absurd h hi + have hexp : 𝔼[signOf (hadamard a)] = 0 := by + rw [signOf_hadamard, expect_parityFun, if_neg hne] + rw [expect_signOf] at hexp + linarith + +/-! ### The bilinear form -/ + +/-- The bilinear form of `c`, contracted against `y`: the linear form in `x` +obtained by fixing the second argument. -/ +def tensorRow (c : Cube (n * n)) (y : Cube n) : Cube n := + fun i => ∑ j, c (finProdFinEquiv (i, j)) * y j + +/-- **Row decomposition.** Reading the quadratic table at an outer product is a +*linear* reading in the first argument, with coefficients contracted against the +second. This is what lets the one-variable balance lemma be applied inside a +two-variable check. -/ +theorem hadamard_tensor_row (c : Cube (n * n)) (x y : Cube n) : + hadamard c (tensor x y) = hadamard (tensorRow c y) x := by + classical + have hre : ∀ f : Fin n × Fin n → ZMod 2, + ∑ k : Fin (n * n), f (finProdFinEquiv.symm k) = ∑ p : Fin n × Fin n, f p := by + intro f + exact Fintype.sum_equiv finProdFinEquiv.symm (fun k => f (finProdFinEquiv.symm k)) f + fun k => rfl + have hlhs : hadamard c (tensor x y) + = ∑ p : Fin n × Fin n, c (finProdFinEquiv p) * (x p.1 * y p.2) := by + show ∑ k : Fin (n * n), c k * (tensor x y) k = _ + have hstep : ∑ k : Fin (n * n), c k * (tensor x y) k + = ∑ p : Fin n × Fin n, + c (finProdFinEquiv p) * (x p.1 * y p.2) := by + refine Eq.trans ?_ (hre fun p => c (finProdFinEquiv p) * (x p.1 * y p.2)) + refine Finset.sum_congr rfl fun k _ => ?_ + show c k * ((x (finProdFinEquiv.symm k).1) * (y (finProdFinEquiv.symm k).2)) + = c (finProdFinEquiv (finProdFinEquiv.symm k)) + * (x (finProdFinEquiv.symm k).1 * y (finProdFinEquiv.symm k).2) + rw [Equiv.apply_symm_apply] + exact hstep + rw [hlhs] + show _ = ∑ i, (∑ j, c (finProdFinEquiv (i, j)) * y j) * x i + rw [Fintype.sum_prod_type] + refine Finset.sum_congr rfl fun i _ => ?_ + rw [Finset.sum_mul] + refine Finset.sum_congr rfl fun j _ => ?_ + ring + +/-- Probability is monotone. The Fourier layer has the union bound and +complements but not this. -/ +theorem prob_mono {P Q : Cube n → Prop} (h : ∀ x, P x → Q x) : Pr[P] ≤ Pr[Q] := by + classical + simp only [BooleanAnalysis.prob, expect_unfold, BooleanAnalysis.indicator] + refine mul_le_mul_of_nonneg_left (Finset.sum_le_sum fun x _ => ?_) (by positivity) + by_cases hp : P x + · rw [if_pos hp, if_pos (h x hp)] + · rw [if_neg hp] + split_ifs <;> norm_num + +/-- **The bilinear form is nonzero on many rows.** If the quadratic table's +error `c` is nonzero, then for at least half the `y` the contracted linear form +is nonzero — the first half of the `1/4` bound. -/ +theorem prob_tensorRow_ne_zero (c : Cube (n * n)) (hc : c ≠ 0) : + 1 / 2 ≤ Pr[fun y => tensorRow c y ≠ 0] := by + classical + obtain ⟨k, hk⟩ : ∃ k, c k ≠ 0 := by + by_contra hcon + push Not at hcon + exact hc (funext hcon) + set i := (finProdFinEquiv.symm k).1 with hi + set j := (finProdFinEquiv.symm k).2 with hj + have hkij : k = finProdFinEquiv (i, j) := by + rw [hi, hj, Prod.mk.eta, Equiv.apply_symm_apply] + set d : Cube n := fun j' => c (finProdFinEquiv (i, j')) with hd + have hdne : d ≠ 0 := by + intro h0 + apply hk + rw [hkij] + have := congrFun h0 j + rw [hd] at this + exact this + have hrow : ∀ y : Cube n, (tensorRow c y) i = hadamard d y := fun y => rfl + have hsub : ∀ y : Cube n, hadamard d y ≠ 0 → tensorRow c y ≠ 0 := by + intro y hy hzero + apply hy + rw [← hrow y, hzero] + rfl + calc (1 : ℝ) / 2 = Pr[fun y => hadamard d y ≠ 0] := (prob_hadamard_ne_zero d hdne).symm + _ ≤ Pr[fun y => tensorRow c y ≠ 0] := prob_mono hsub + +theorem expect_mono {f g : BooleanFunction n} (h : ∀ x, f x ≤ g x) : 𝔼[f] ≤ 𝔼[g] := by + rw [expect_unfold, expect_unfold] + exact mul_le_mul_of_nonneg_left (Finset.sum_le_sum fun x _ => h x) (by positivity) + +theorem expect_const_mul (c : ℝ) (f : BooleanFunction n) : + 𝔼[fun x => c * f x] = c * 𝔼[f] := by + rw [expect_unfold, expect_unfold, ← Finset.mul_sum] + ring + +/-- **A nonzero bilinear form is nonzero on a quarter of all pairs.** For at +least half the second arguments the contracted form is nonzero, and each such +form is nonzero on exactly half the first arguments. + +This is the quantitative heart of the consistency check: a prover whose +quadratic table differs from the tensor square fails the check on at least a +quarter of the pairs, so passing it more often than that forces the tables to +agree exactly. -/ +theorem prob₂_tensor_ne_zero (c : Cube (n * n)) (hc : c ≠ 0) : + 1 / 4 ≤ Pr₂[fun y x => hadamard c (tensor x y) ≠ 0] := by + classical + have hpoint : ∀ y : Cube n, + (1 / 2 : ℝ) * (if tensorRow c y ≠ 0 then (1 : ℝ) else 0) + ≤ 𝔼[BooleanAnalysis.indicator (fun x => hadamard c (tensor x y) ≠ 0)] := by + intro y + have hcond : (fun x => hadamard c (tensor x y) ≠ 0) + = (fun x => hadamard (tensorRow c y) x ≠ 0) := by + funext x + rw [hadamard_tensor_row] + by_cases hy : tensorRow c y ≠ 0 + · rw [if_pos hy, mul_one, hcond] + exact le_of_eq (prob_hadamard_ne_zero _ hy).symm + · rw [if_neg hy, mul_zero] + exact BooleanAnalysis.Internal.prob_nonneg _ + have hhalf := prob_tensorRow_ne_zero c hc + calc (1 : ℝ) / 4 = (1 / 2) * (1 / 2) := by norm_num + _ ≤ (1 / 2) * Pr[fun y => tensorRow c y ≠ 0] := by linarith + _ = 𝔼[fun y => (1 / 2 : ℝ) * (if tensorRow c y ≠ 0 then (1 : ℝ) else 0)] := by + rw [expect_const_mul] + congr 1 + rw [BooleanAnalysis.prob] + congr 1 + funext y + simp [BooleanAnalysis.indicator] + _ ≤ Pr₂[fun y x => hadamard c (tensor x y) ≠ 0] := expect_mono hpoint + +/-! ### Approximate consistency -/ + +theorem hadamard_add {m : ℕ} (b c z : Cube m) : + hadamard (b + c) z = hadamard b z + hadamard c z := by + show ∑ i, (b i + c i) * z i = (∑ i, b i * z i) + (∑ i, c i * z i) + rw [← Finset.sum_add_distrib] + exact Finset.sum_congr rfl fun i _ => by ring + +theorem expect_add (f g : BooleanFunction n) : + 𝔼[fun x => f x + g x] = 𝔼[f] + 𝔼[g] := by + rw [expect_unfold, expect_unfold, expect_unfold, ← mul_add, ← Finset.sum_add_distrib] + +theorem expect_one : 𝔼[fun _ : Cube n => (1 : ℝ)] = 1 := by + rw [expect_unfold, Finset.sum_const, Finset.card_univ, card_cube, nsmul_eq_mul, mul_one] + push_cast + field_simp + +theorem prob₂_compl (P : Cube n → Cube n → Prop) : + Pr₂[P] + Pr₂[fun x y => ¬ P x y] = 1 := by + classical + rw [BooleanAnalysis.prob₂, BooleanAnalysis.prob₂, ← expect_add, ← expect_one (n := n)] + congr 1 + funext x + exact BooleanAnalysis.Internal.prob_compl (P x) + +/-- **Approximate consistency forces the tensor.** A quadratic table passing the +consistency check on more than three quarters of the pairs must be the tensor +square: otherwise their difference is a nonzero bilinear form, which +`prob₂_tensor_ne_zero` says fails on at least a quarter. -/ +theorem eq_tensorAssign_of_prob_consistent (a : Cube n) (b : Cube (n * n)) + (h : 3 / 4 < Pr₂[fun y x => + hadamard b (tensor x y) = hadamard a x * hadamard a y]) : + b = tensorAssign a := by + classical + by_contra hne + have hcne : b + tensorAssign a ≠ 0 := by + intro h0 + apply hne + funext k + have hk := congrFun h0 k + show b k = (tensorAssign a) k + rcases (by decide : ∀ u v : ZMod 2, u + v = 0 → u = v) (b k) ((tensorAssign a) k) hk with h' + exact h' + have hquarter := prob₂_tensor_ne_zero (b + tensorAssign a) hcne + have hfail : (fun y x => hadamard (b + tensorAssign a) (tensor x y) ≠ 0) + = fun y x => ¬ (hadamard b (tensor x y) = hadamard a x * hadamard a y) := by + funext y x + rw [hadamard_add, hadamard_tensor] + have hiff : (hadamard b (tensor x y) + hadamard a x * hadamard a y = 0) + ↔ (hadamard b (tensor x y) = hadamard a x * hadamard a y) := by + rcases (by decide : ∀ u v : ZMod 2, (u + v = 0) ↔ (u = v)) (hadamard b (tensor x y)) + (hadamard a x * hadamard a y) with h' + exact h' + exact propext (not_congr hiff) + rw [hfail] at hquarter + have hcompl := prob₂_compl (fun y x => + hadamard b (tensor x y) = hadamard a x * hadamard a y) + linarith + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/HadamardTester.lean b/Complexitylib/Classes/PCP/Internal/HadamardTester.lean new file mode 100644 index 00000000..13157630 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/HadamardTester.lean @@ -0,0 +1,275 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.Hadamard + +/-! +# The Hadamard tester, as it actually runs + +`Complexitylib.Classes.PCP.Internal.Hadamard` analyses the tester at the +*decoded* level: its statements are about the codewords `hadamard a` and +`hadamard b` that the two proof tables are supposed to be. A tester cannot read +those. It reads the tables it is given, which are only *close* to codewords, and +it recovers codeword values by self-correction — reading two nearby entries and +multiplying. + +This module closes that gap. The tester's whole random string is one point of a +bundled cube (`CubeBlocks`): the first block picks the two query points, and the +remaining blocks supply one correction string per read. Every read is then +correct except with probability `2ε`, the failures are collected by a union +bound, and `prob_le_of_imp_of_good` transfers the observed acceptance +probability to the decoded check that `Hadamard` already knows how to use. + +The one wrinkle is that the consistency check is *bilinear over `𝔽₂`*, not +multiplicative on signs: `⟨a ⊗ a, x ⊗ y⟩ = ⟨a, x⟩ · ⟨a, y⟩` is a product of +bits, and `chi` does not carry products of bits to products of signs. So the +reads' `±1` answers are converted back to bits by `signBit` before being +compared, which is exactly how the check is stated in the literature. + +## Main definitions + +- `Complexity.signBit` — the bit a `±1` answer stands for +- `Complexity.TesterAccepts` — the tester's check on the raw tables +- `Complexity.ReadsCorrect` — the event that every self-corrected read is right + +## Main results + +- `Complexity.prob_reads_correct` — every read is right except with + probability `4ε + 2ε'` +-/ + +@[expose] public section + +namespace Complexity + +open BooleanAnalysis + +variable {n : ℕ} + +/-! ### Reading a sign as a bit -/ + +/-- The bit a `±1` answer stands for. -/ +noncomputable def signBit (r : ℝ) : ZMod 2 := if r = 1 then 0 else 1 + +/-- Signs and bits correspond: `signBit` inverts `chi`. -/ +theorem signBit_chi (u : ZMod 2) : signBit (chi u) = u := by + rcases (by decide : ∀ u : ZMod 2, u = 0 ∨ u = 1) u with h | h <;> subst h + · norm_num [signBit, BooleanAnalysis.chi] + · norm_num [signBit, BooleanAnalysis.chi] + +/-- **A self-corrected read is right.** The decoded-level restatement of +`local_correctability`: the value returned is the codeword's bit, read as a +sign. -/ +theorem prob_read_ge {m : ℕ} (f : BooleanFunction m) (hf : IsBooleanValued f) + (a : Cube m) {ε : ℝ} (hc : IsClose f (signOf (hadamard a)) ε) (x : Cube m) : + 1 - 2 * ε ≤ Pr[fun r : Cube m => f r * f (x + r) = chi (hadamard a x)] := by + classical + have hS : IsClose f (χ (Finset.univ.filter fun i => a i = 1)) ε := by + rwa [← signOf_hadamard] + have heq : (χ (Finset.univ.filter fun i => a i = 1)) x = chi (hadamard a x) := by + rw [← signOf_hadamard] + rfl + have h := local_correctability f hf (Finset.univ.filter fun i => a i = 1) hS x + rw [heq] at h + exact h + +/-! ### The tester -/ + +/-- The first query point: the second half of the query block. -/ +def qX (z : Cube ((n + n) + (n + (n + n * n)))) : Cube n := rightBlock (leftBlock z) + +/-- The second query point: the first half of the query block. -/ +def qY (z : Cube ((n + n) + (n + (n + n * n)))) : Cube n := leftBlock (leftBlock z) + +/-- The correction string for the first read of the linear table. -/ +def cX (z : Cube ((n + n) + (n + (n + n * n)))) : Cube n := leftBlock (rightBlock z) + +/-- The correction string for the second read of the linear table. -/ +def cY (z : Cube ((n + n) + (n + (n + n * n)))) : Cube n := + leftBlock (rightBlock (rightBlock z)) + +/-- The correction string for the read of the quadratic table. -/ +def cQ (z : Cube ((n + n) + (n + (n + n * n)))) : Cube (n * n) := + rightBlock (rightBlock (rightBlock z)) + +/-- **The tester's check**, made on the raw tables: the self-corrected value of +the quadratic table at `x ⊗ y` must be the product, as bits, of the +self-corrected values of the linear table at `x` and at `y`. -/ +def TesterAccepts (f : BooleanFunction n) (g : BooleanFunction (n * n)) + (z : Cube ((n + n) + (n + (n + n * n)))) : Prop := + signBit (g (cQ z) * g (tensor (qX z) (qY z) + cQ z)) + = signBit (f (cX z) * f (qX z + cX z)) * signBit (f (cY z) * f (qY z + cY z)) + +/-- The event that all three self-corrected reads return the codeword's value. -/ +def ReadsCorrect (a : Cube n) (b : Cube (n * n)) (f : BooleanFunction n) + (g : BooleanFunction (n * n)) (z : Cube ((n + n) + (n + (n + n * n)))) : Prop := + f (cX z) * f (qX z + cX z) = chi (hadamard a (qX z)) + ∧ f (cY z) * f (qY z + cY z) = chi (hadamard a (qY z)) + ∧ g (cQ z) * g (tensor (qX z) (qY z) + cQ z) + = chi (hadamard b (tensor (qX z) (qY z))) + +/-- **Every read is right, at once.** The query points are chosen by the first +block and the corrections by the rest, so `prob_blocks_ge` fixes the points +before the corrections are drawn and the three failure probabilities simply +add. -/ +theorem prob_reads_correct (a : Cube n) (b : Cube (n * n)) + (f : BooleanFunction n) (hf : IsBooleanValued f) {ε : ℝ} + (hfc : IsClose f (signOf (hadamard a)) ε) + (g : BooleanFunction (n * n)) (hg : IsBooleanValued g) {ε' : ℝ} + (hgc : IsClose g (signOf (hadamard b)) ε') : + 1 - (4 * ε + 2 * ε') ≤ Pr[ReadsCorrect a b f g] := by + classical + have hsplit : (1 : ℝ) - (4 * ε + 2 * ε') = 1 - (2 * ε + (2 * ε + 2 * ε')) := by ring + rw [hsplit] + refine prob_blocks_ge (fun u w => + f (leftBlock w) * f (rightBlock u + leftBlock w) = chi (hadamard a (rightBlock u)) + ∧ f (leftBlock (rightBlock w)) * f (leftBlock u + leftBlock (rightBlock w)) + = chi (hadamard a (leftBlock u)) + ∧ g (rightBlock (rightBlock w)) + * g (tensor (rightBlock u) (leftBlock u) + rightBlock (rightBlock w)) + = chi (hadamard b (tensor (rightBlock u) (leftBlock u)))) _ fun u => ?_ + · refine prob_and_ge ?_ (prob_and_ge ?_ ?_) + · have h : Pr[fun w : Cube (n + (n + n * n)) => + f (leftBlock w) * f (rightBlock u + leftBlock w) + = chi (hadamard a (rightBlock u))] + = Pr[fun r : Cube n => + f r * f (rightBlock u + r) = chi (hadamard a (rightBlock u))] := + prob_leftBlock (fun r : Cube n => + f r * f (rightBlock u + r) = chi (hadamard a (rightBlock u))) + rw [h] + exact prob_read_ge f hf a hfc (rightBlock u) + · have h : Pr[fun w : Cube (n + (n + n * n)) => + f (leftBlock (rightBlock w)) * f (leftBlock u + leftBlock (rightBlock w)) + = chi (hadamard a (leftBlock u))] + = Pr[fun r : Cube n => + f r * f (leftBlock u + r) = chi (hadamard a (leftBlock u))] := + prob_leftBlock_rightBlock (fun r : Cube n => + f r * f (leftBlock u + r) = chi (hadamard a (leftBlock u))) + rw [h] + exact prob_read_ge f hf a hfc (leftBlock u) + · have h : Pr[fun w : Cube (n + (n + n * n)) => + g (rightBlock (rightBlock w)) + * g (tensor (rightBlock u) (leftBlock u) + rightBlock (rightBlock w)) + = chi (hadamard b (tensor (rightBlock u) (leftBlock u)))] + = Pr[fun r : Cube (n * n) => + g r * g (tensor (rightBlock u) (leftBlock u) + r) + = chi (hadamard b (tensor (rightBlock u) (leftBlock u)))] := + prob_rightBlock_rightBlock (fun r : Cube (n * n) => + g r * g (tensor (rightBlock u) (leftBlock u) + r) + = chi (hadamard b (tensor (rightBlock u) (leftBlock u)))) + rw [h] + exact prob_read_ge g hg b hgc (tensor (rightBlock u) (leftBlock u)) + +/-- **Consistency forces the tensor, on raw tables.** The consistency check +passing often enough on tables close to codewords forces the quadratic codeword +to be the tensor square of the linear one. -/ +theorem eq_tensorAssign_of_prob_tester (a : Cube n) (b : Cube (n * n)) + (f : BooleanFunction n) (hf : IsBooleanValued f) {ε : ℝ} + (hfc : IsClose f (signOf (hadamard a)) ε) + (g : BooleanFunction (n * n)) (hg : IsBooleanValued g) {ε' : ℝ} + (hgc : IsClose g (signOf (hadamard b)) ε') + (haccept : 3 / 4 + (4 * ε + 2 * ε') < Pr[TesterAccepts f g]) : + b = tensorAssign a := by + classical + have hgood := prob_reads_correct a b f hf hfc g hg hgc + have htrans := prob_le_of_imp_of_good (E := TesterAccepts f g) + (F := fun z : Cube ((n + n) + (n + (n + n * n))) => + hadamard b (tensor (rightBlock (leftBlock z)) (leftBlock (leftBlock z))) + = hadamard a (rightBlock (leftBlock z)) * hadamard a (leftBlock (leftBlock z))) + (A := ReadsCorrect a b f g) fun z hE hA => by + have hE' : signBit (chi (hadamard b (tensor (qX z) (qY z)))) + = signBit (chi (hadamard a (qX z))) * signBit (chi (hadamard a (qY z))) := by + rw [← hA.1, ← hA.2.1, ← hA.2.2] + exact hE + rw [signBit_chi, signBit_chi, signBit_chi] at hE' + exact hE' + have hpair : Pr[fun z : Cube ((n + n) + (n + (n + n * n))) => + hadamard b (tensor (rightBlock (leftBlock z)) (leftBlock (leftBlock z))) + = hadamard a (rightBlock (leftBlock z)) * hadamard a (leftBlock (leftBlock z))] + = Pr₂[fun y x => hadamard b (tensor x y) = hadamard a x * hadamard a y] := + prob_pair_block (fun y x => hadamard b (tensor x y) = hadamard a x * hadamard a y) + rw [hpair] at htrans + exact eq_tensorAssign_of_prob_consistent a b (by linarith) + +/-! ### Completeness -/ + +/-- A Hadamard codeword is linear in the query point. -/ +theorem hadamard_add_right {m : ℕ} (a x r : Cube m) : + hadamard a (x + r) = hadamard a x + hadamard a r := by + show ∑ i, a i * (x i + r i) = (∑ i, a i * x i) + (∑ i, a i * r i) + rw [← Finset.sum_add_distrib] + exact Finset.sum_congr rfl fun i _ => by ring + +/-- **Self-correction is exact on an honest table.** The two reads are +`chi ⟨a, r⟩` and `chi (⟨a, x⟩ + ⟨a, r⟩)`, and their product telescopes because +`chi` turns the `𝔽₂` sum into a product of signs and `2⟨a, r⟩ = 0`. -/ +theorem corrected_read_honest {m : ℕ} (a x r : Cube m) : + signOf (hadamard a) r * signOf (hadamard a) (x + r) = chi (hadamard a x) := by + show chi (hadamard a r) * chi (hadamard a (x + r)) = chi (hadamard a x) + rw [hadamard_add_right, ← BooleanAnalysis.Internal.chi_add] + congr 1 + have h2 : hadamard a r + hadamard a r = 0 := by + rcases (by decide : ∀ u : ZMod 2, u + u = 0) (hadamard a r) with h + exact h + calc hadamard a r + (hadamard a x + hadamard a r) + = hadamard a x + (hadamard a r + hadamard a r) := by ring + _ = hadamard a x := by rw [h2, add_zero] + +/-- **The honest proof passes every read.** -/ +theorem readsCorrect_of_honest (a : Cube n) (z : Cube ((n + n) + (n + (n + n * n)))) : + ReadsCorrect a (tensorAssign a) (signOf (hadamard a)) + (signOf (hadamard (tensorAssign a))) z := + ⟨corrected_read_honest a (qX z) (cX z), corrected_read_honest a (qY z) (cY z), + corrected_read_honest (tensorAssign a) (tensor (qX z) (qY z)) (cQ z)⟩ + +/-- **Completeness of the tester as it runs.** The honest proof — the Hadamard +encoding of an assignment together with the encoding of its tensor square — is +accepted on every random string, so with probability one. -/ +theorem testerAccepts_of_honest (a : Cube n) (z : Cube ((n + n) + (n + (n + n * n)))) : + TesterAccepts (signOf (hadamard a)) (signOf (hadamard (tensorAssign a))) z := by + have hr := readsCorrect_of_honest a z + show signBit (signOf (hadamard (tensorAssign a)) (cQ z) + * signOf (hadamard (tensorAssign a)) (tensor (qX z) (qY z) + cQ z)) = _ + rw [hr.1, hr.2.1, hr.2.2, signBit_chi, signBit_chi, signBit_chi] + exact hadamard_tensor a (qX z) (qY z) + +/-! ### The constraint check -/ + +/-- **The tester's constraint check**, made on the raw tables: the constraint's +quadratic part is read from `g`, its linear part from `f`, both by +self-correction, and the two bits plus the constant must cancel. -/ +def ConstraintAccepts (f : BooleanFunction n) (g : BooleanFunction (n * n)) + (C : QuadConstraint n) (z : Cube (n + n * n)) : Prop := + signBit (g (rightBlock z) * g (C.quad + rightBlock z)) + + signBit (f (leftBlock z) * f (C.lin + leftBlock z)) + C.const = 0 + +/-- Both reads of the constraint check are right except with probability +`2ε + 2ε'`. -/ +theorem prob_constraint_reads (a : Cube n) (b : Cube (n * n)) (C : QuadConstraint n) + (f : BooleanFunction n) (hf : IsBooleanValued f) {ε : ℝ} + (hfc : IsClose f (signOf (hadamard a)) ε) + (g : BooleanFunction (n * n)) (hg : IsBooleanValued g) {ε' : ℝ} + (hgc : IsClose g (signOf (hadamard b)) ε') : + 1 - (2 * ε + 2 * ε') ≤ Pr[fun z : Cube (n + n * n) => + f (leftBlock z) * f (C.lin + leftBlock z) = chi (hadamard a C.lin) + ∧ g (rightBlock z) * g (C.quad + rightBlock z) = chi (hadamard b C.quad)] := by + classical + refine prob_and_ge ?_ ?_ + · have h : Pr[fun z : Cube (n + n * n) => + f (leftBlock z) * f (C.lin + leftBlock z) = chi (hadamard a C.lin)] + = Pr[fun r : Cube n => f r * f (C.lin + r) = chi (hadamard a C.lin)] := + prob_leftBlock (fun r : Cube n => f r * f (C.lin + r) = chi (hadamard a C.lin)) + rw [h] + exact prob_read_ge f hf a hfc C.lin + · have h : Pr[fun z : Cube (n + n * n) => + g (rightBlock z) * g (C.quad + rightBlock z) = chi (hadamard b C.quad)] + = Pr[fun r : Cube (n * n) => g r * g (C.quad + r) = chi (hadamard b C.quad)] := + prob_rightBlock (fun r : Cube (n * n) => + g r * g (C.quad + r) = chi (hadamard b C.quad)) + rw [h] + exact prob_read_ge g hg b hgc C.quad + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/KilledCSP.lean b/Complexitylib/Classes/PCP/Internal/KilledCSP.lean new file mode 100644 index 00000000..e33dd8bf --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/KilledCSP.lean @@ -0,0 +1,229 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.KilledWalk +public import Complexitylib.Classes.PCP.Internal.NumEncPi +public import Complexitylib.Classes.PCP.Internal.RegCSP + +/-! +# Powering a constraint system along killed walks + +Dinur's gap amplification, over the walk law of `KilledWalk`. The vertices are +unchanged; the constraints are indexed by killed walks, and the alphabet is +*opinions* — but now indexed by walks of **any** length up to `T`, so that no +padding is ever needed. + +## Opinions + +A label at `v` is a function `VarWalk G T → α`: for every walk out of `v` of +length at most `T`, a claim about the label of its endpoint. The index type is +finite of size `∑_{ℓ ≤ T} deg ^ ℓ`, so the alphabet stays a constant +`|α| ^ (∑_{ℓ ≤ T} deg ^ ℓ)`, independent of the number of vertices — which is +what makes the alphabet-reduction step afterwards possible. + +Variable-length indices are the point of the design. Were the indices instead +walks of one fixed length, a short prefix would have to be padded out with +self-loops, and the padded indices would form a vanishing sub-cube that the +plurality bound cannot see — which sinks soundness. `KilledWalk` records the +counting behind that failure. + +## The constraint + +On a killed walk `(v, x)` with effective length `ℓ`, **every** step `i < ℓ` is +checked — no window is needed, since the start's opinion reaches `v i` through +the walk's own first `i` steps and the end's reaches `v (i+1)` through the +reversed walk's first `ℓ - (i+1)` steps. Both indices are genuine walks, of the +exact lengths the walk itself provides. + +## Main definitions + +- `VarWalk`, `KOpinion` — short walks, and the alphabet of opinions about them +- `RegGraph.startIdx`, `RegGraph.endIdx` — the two indices a step is read at +- `RegCSP.killedPow` — the powered system +- `RegCSP.kTruthful` — the opinion assignment induced by an assignment of `R` + +## Main results + +- `RegGraph.walkEnd_startIdx`, `RegGraph.walkEnd_endIdx` — the two indices name + the two ends of the `i`-th dart +- `RegCSP.rel_killedPow_iff` — the constraint, unfolded +- `RegCSP.satisfiable_killedPow_of_satisfiable` — perfect completeness +- `RegCSP.not_satisfies_killedPow_of_faulty` — the soundness witness: a failed + step with truthful opinions at both ends breaks the constraint +-/ + +@[expose] public section + +namespace Complexity + +/-- A walk out of a vertex, of any length up to `T`. -/ +abbrev VarWalk (G : RegGraph) (T : ℕ) : Type := Σ ℓ : Fin (T + 1), Fin ℓ.val → G.D + +/-- A label of the killed power: a claim about the endpoint of every walk of +length at most `T` out of the vertex. -/ +abbrev KOpinion (G : RegGraph) (T : ℕ) (α : Type) : Type := VarWalk G T → α + +namespace RegGraph + +variable (G : RegGraph) + +/-! ### The two indices a step is read at -/ + +/-- Where the start of a killed walk holds its opinion about the walk's `i`-th +vertex: at the walk's own first `i` steps. -/ +def startIdx {T ℓ : ℕ} (hℓ : ℓ ≤ T) (w : Fin ℓ → G.D) (i : Fin ℓ) : VarWalk G T := + ⟨⟨i.val, by have := i.isLt; omega⟩, fun j => w (Fin.castLE (le_of_lt i.isLt) j)⟩ + +/-- Where the end of a killed walk holds its opinion about the walk's +`(i+1)`-st vertex: at the reversed walk's first `ℓ - (i+1)` steps. -/ +def endIdx {T ℓ : ℕ} (hℓ : ℓ ≤ T) (v : G.V) (w : Fin ℓ → G.D) (i : Fin ℓ) : VarWalk G T := + ⟨⟨ℓ - (i.val + 1), by have := i.isLt; omega⟩, + fun j : Fin (ℓ - (i.val + 1)) => G.revWalk v w ⟨j.val, by have := j.isLt; omega⟩⟩ + +/-- The start's index names the `i`-th vertex of the walk. -/ +theorem walkEnd_startIdx {T ℓ : ℕ} (hℓ : ℓ ≤ T) (v : G.V) (w : Fin ℓ → G.D) (i : Fin ℓ) : + G.walkEnd (G.startIdx hℓ w i).1.val v (G.startIdx hℓ w i).2 = G.walkAt ℓ v w i.val := by + rw [G.walkAt_eq_walkEnd_prefix v w i.val (le_of_lt i.isLt)] + rfl + +/-- The end's index names the `(i+1)`-st vertex of the walk. -/ +theorem walkEnd_endIdx {T ℓ : ℕ} (hℓ : ℓ ≤ T) (v : G.V) (w : Fin ℓ → G.D) (i : Fin ℓ) : + G.walkEnd (G.endIdx hℓ v w i).1.val (G.walkEnd ℓ v w) (G.endIdx hℓ v w i).2 + = G.walkAt ℓ v w (i.val + 1) := by + have hle : ℓ - (i.val + 1) ≤ ℓ := by omega + have key : G.walkAt ℓ (G.walkEnd ℓ v w) (G.revWalk v w) (ℓ - (i.val + 1)) + = G.walkAt ℓ v w (i.val + 1) := by + rw [G.walkAt_revWalk v w _ hle] + congr 1 + have := i.isLt + omega + rw [← key, G.walkAt_eq_walkEnd_prefix (G.walkEnd ℓ v w) (G.revWalk v w) _ hle] + rfl + +/-! ### The effective data of a killed dart -/ + +/-- The effective length of a killed dart. -/ +def kLen {T q : ℕ} (x : (Fin T → G.D) × (Fin T → Fin q)) : ℕ := stopAt x.2 + +theorem kLen_le {T q : ℕ} (x : (Fin T → G.D) × (Fin T → Fin q)) : G.kLen x ≤ T := + stopAt_le x.2 + +/-- The effective walk of a killed dart. -/ +def kWalk {T q : ℕ} (x : (Fin T → G.D) × (Fin T → Fin q)) : Fin (G.kLen x) → G.D := + G.preWalk x.1 (stopAt_le x.2) + +theorem killedEnd_eq {T q : ℕ} (v : G.V) (x : (Fin T → G.D) × (Fin T → Fin q)) : + G.killedEnd v x.1 x.2 = G.walkEnd (G.kLen x) v (G.kWalk x) := rfl + +end RegGraph + +namespace RegCSP + +variable {α : Type} (R : RegCSP α) (q T : ℕ) (hq : 0 < q) + +/-- The killed power of a constraint system: one constraint per killed walk, +checking `R`'s constraint at every step of the effective walk, between the +opinions the two ends hold about that step's two vertices. -/ +def killedPow (R : RegCSP α) (q T : ℕ) (hq : 0 < q) : RegCSP (KOpinion R.graph T α) where + graph := R.graph.killedPower q T hq + rel v x a b := + decide (∀ i : Fin (R.graph.kLen x), + R.rel (R.graph.walkAt (R.graph.kLen x) v (R.graph.kWalk x) i.val) (R.graph.kWalk x i) + (a (R.graph.startIdx (R.graph.kLen_le x) (R.graph.kWalk x) i)) + (b (R.graph.endIdx (R.graph.kLen_le x) v (R.graph.kWalk x) i)) = true) + +@[simp] theorem graph_killedPow : + (R.killedPow q T hq).graph = R.graph.killedPower q T hq := rfl + +/-- Powering leaves the vertices alone, so they keep their numbering. -/ +noncomputable instance [NumEnc R.graph.V] : NumEnc (R.killedPow q T hq).graph.V := + inferInstanceAs (NumEnc R.graph.V) + +/-- A killed walk is a tuple of darts and a tuple of coins; both are numbered +digit by digit, so an algorithm can read the walk off. -/ +noncomputable instance [NumEnc R.graph.D] : NumEnc (R.killedPow q T hq).graph.D := + inferInstanceAs (NumEnc ((Fin T → R.graph.D) × (Fin T → Fin q))) + +theorem rel_killedPow_iff (v : R.graph.V) (x : (Fin T → R.graph.D) × (Fin T → Fin q)) + (a b : KOpinion R.graph T α) : + (R.killedPow q T hq).rel v x a b = true + ↔ ∀ i : Fin (R.graph.kLen x), + R.rel (R.graph.walkAt (R.graph.kLen x) v (R.graph.kWalk x) i.val) (R.graph.kWalk x i) + (a (R.graph.startIdx (R.graph.kLen_le x) (R.graph.kWalk x) i)) + (b (R.graph.endIdx (R.graph.kLen_le x) v (R.graph.kWalk x) i)) = true := by + exact decide_eq_true_iff + +/-! ### Completeness -/ + +/-- The opinion assignment induced by an assignment of `R`: every claim is the +truth. -/ +def kTruthful (σ : R.Assignment) : (R.killedPow q T hq).Assignment := + fun v w => σ (R.graph.walkEnd w.1.val v w.2) + +/-- A satisfying assignment of `R` makes every killed-walk constraint hold. -/ +theorem satisfies_killedPow_truthful {σ : R.Assignment} (hσ : ∀ p, R.Satisfies σ p) + (x : (R.killedPow q T hq).Dart) : + (R.killedPow q T hq).Satisfies (R.kTruthful q T hq σ) x := by + obtain ⟨v, y⟩ := x + rw [Satisfies, satisfies] + dsimp only + rw [show (R.killedPow q T hq).graph.nbr v y = R.graph.killedEnd v y.1 y.2 from rfl] + rw [rel_killedPow_iff] + intro i + have hstart : R.kTruthful q T hq σ v + (R.graph.startIdx (R.graph.kLen_le y) (R.graph.kWalk y) i) + = σ (R.graph.walkAt (R.graph.kLen y) v (R.graph.kWalk y) i.val) := by + rw [kTruthful] + exact congrArg σ (R.graph.walkEnd_startIdx (R.graph.kLen_le y) v (R.graph.kWalk y) i) + have hend : R.kTruthful q T hq σ (R.graph.killedEnd v y.1 y.2) + (R.graph.endIdx (R.graph.kLen_le y) v (R.graph.kWalk y) i) + = σ (R.graph.walkAt (R.graph.kLen y) v (R.graph.kWalk y) (i.val + 1)) := by + rw [kTruthful, R.graph.killedEnd_eq] + exact congrArg σ (R.graph.walkEnd_endIdx (R.graph.kLen_le y) v (R.graph.kWalk y) i) + rw [hstart, hend] + have hdart := hσ (R.graph.walkAt (R.graph.kLen y) v (R.graph.kWalk y) i.val, + R.graph.kWalk y i) + rw [Satisfies, satisfies] at hdart + dsimp only at hdart + rw [← R.graph.walkAt_succ_of_lt v (R.graph.kWalk y) i.isLt] at hdart + exact hdart + +/-- **Perfect completeness.** -/ +theorem satisfiable_killedPow_of_satisfiable (hR : R.Satisfiable) : + (R.killedPow q T hq).Satisfiable := by + obtain ⟨σ, hσ⟩ := hR + exact ⟨R.kTruthful q T hq σ, fun x => R.satisfies_killedPow_truthful q T hq hσ x⟩ + +/-! ### The soundness witness -/ + +/-- A step of the effective walk that `σ` fails, whose two vertices both ends +shape every soundness count is built from. -/ +theorem not_satisfies_killedPow_of_faulty {σ : R.Assignment} + (A : (R.killedPow q T hq).Assignment) (v : R.graph.V) + (y : (Fin T → R.graph.D) × (Fin T → Fin q)) (i : Fin (R.graph.kLen y)) + (hfault : ¬ R.Satisfies σ + (R.graph.walkAt (R.graph.kLen y) v (R.graph.kWalk y) i.val, R.graph.kWalk y i)) + (htruth₁ : A v (R.graph.startIdx (R.graph.kLen_le y) (R.graph.kWalk y) i) + = σ (R.graph.walkAt (R.graph.kLen y) v (R.graph.kWalk y) i.val)) + (htruth₂ : A (R.graph.killedEnd v y.1 y.2) + (R.graph.endIdx (R.graph.kLen_le y) v (R.graph.kWalk y) i) + = σ (R.graph.walkAt (R.graph.kLen y) v (R.graph.kWalk y) (i.val + 1))) : + ¬ (R.killedPow q T hq).Satisfies A (v, y) := by + intro hsat + rw [Satisfies, satisfies] at hsat + dsimp only at hsat + rw [show (R.killedPow q T hq).graph.nbr v y = R.graph.killedEnd v y.1 y.2 from rfl, + rel_killedPow_iff] at hsat + have hi := hsat i + rw [htruth₁, htruth₂] at hi + rw [Satisfies, satisfies] at hfault + dsimp only at hfault + rw [← R.graph.walkAt_succ_of_lt v (R.graph.kWalk y) i.isLt] at hfault + exact hfault hi + +end RegCSP + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/KilledFirstMoment.lean b/Complexitylib/Classes/PCP/Internal/KilledFirstMoment.lean new file mode 100644 index 00000000..68dd609f --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/KilledFirstMoment.lean @@ -0,0 +1,1155 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.KilledPlurality +public import Complexitylib.Classes.PCP.Internal.WalkSplit +public import Complexitylib.Classes.PCP.Internal.DartCorrelation +public import Complexitylib.Classes.PCP.Internal.SecondMoment +public import Mathlib.Algebra.BigOperators.Intervals + +/-! +# The first moment of Dinur's powering step + +Counting, for a fixed faulty dart of `R` and a fixed crossing position, the +killed-power constraints that the dart breaks — those whose walk crosses it and +whose two ends both hold the decoded opinion about the dart's endpoints. + +`WalkSplit.card_label_crossing` already counts such labels as +`(prefix count) * (suffix count) * (fibre weight)`, for *arbitrary* conditions +on the two pieces. Here the conditions are the ones the powered constraint +actually reads, and then each count is by definition a `truthCount` — the +quantity `KilledPlurality.card_le_mul_sum_truthCount` bounds from below. + +That is the whole point of the killed walk law: the prefix and suffix conditions +concern different vertices and *independent* lengths, so summing over both +positions multiplies two plurality bounds instead of entangling them. + +## Main results + +- `RegCSP.opinionOf_eq_startIdx`, `RegCSP.opinionOf_eq_endIdx` — the counted + conditions are the constraint's own two terms +- `RegCSP.card_good_crossing` — the count, as a product of two `truthCount`s and + the fibre weight +- `RegCSP.not_satisfies_of_good_crossing` — every counted crossing breaks its + constraint +- `weight_factor` — the crossing weight is the product of the two plurality + weights, up to a fixed normaliser +- `sum_crossing_factor`, `sum_goodCount_factor` — hence the double sum over + positions is a product of two weighted sums +- `geom_tail_le`, `RegCSP.truthCount_le` — the discarded positions carry + geometrically little weight +- `RegCSP.card_le_mul_sum_truthCount_half` — plurality survives the restriction + to half the range +- `RegCSP.card_good_crossing_sq` — the count indexed by the two lengths +- `RegCSP.halfSum`, `pluralityLoss`, `RegCSP.per_dart_lower` — the first moment + for a single dart of `R` +- `RegCSP.sum_dart_lower` — summed over all the failed darts +- `RegCSP.goodCrossings`, `RegCSP.mem_unsatDarts_of_goodCrossings_nonempty` — the + count the second-moment method uses, and its support condition +- `RegCSP.sum_sq_goodCrossings` — its second moment, as a sum over pairs +- `RegCSP.goodPos`, `RegCSP.card_pairs_goodPos` — the same positions indexed by + naturals, so summation order can be exchanged +- `RegCSP.sum_card_pairs_eq` — that exchange, carried out +- `RegCSP.card_both_good_le` — two good crossings imply two failed crossings of + the underlying walk +- `RegCSP.sum_pairs_bound` — the second moment's pair term, bounded +- `RegCSP.sum_sq_goodCrossings_le` — the second moment of the crossing count +- `RegCSP.sum_card_goodPos_eq` — the first moment, with counting exchanged +- `RegCSP.sum_over_len_le` — summing over effective lengths stays within the + constraints good at a position +- `RegCSP.mem_goodPos_of_crossing` — a counted crossing of a failed dart is a + good position +- `RegCSP.crossingSet`, `RegCSP.sum_crossingSet_le` — the counted sets, and that + different darts contribute disjointly +- `RegCSP.sum_len_pos_le`, `RegCSP.sum_Cd_le_sum_goodCrossings` — the first + moment is bounded by the total crossing count +- `RegCSP.crossCount`, `RegCSP.sum_goodCrossings_ge` — the first moment in closed + form +- `RegCSP.card_unsatDarts_ge` — the second-moment bound on unsatisfied + constraints +-/ + +@[expose] public section + +namespace Complexity + +/-- **The weights factorise.** The fibre weight of a walk of length `i + j + 1` +is, up to the fixed normaliser `deg ^ (T+1) * q ^ T`, the product of the weights +of lengths `i` and `j` times `q - 1`. + +This identity is why the first moment splits: summing a product +`truthCount a i * truthCount b j` against the crossing weight is the same as +multiplying two sums each weighted exactly as the plurality bound weights them. +Geometric weights are what make this work, and geometric weights are what the +killed walk law produces. -/ +theorem weight_factor (deg q : ℕ) {T i j : ℕ} (h : i + j + 1 < T) : + (deg ^ (T - (i + j + 1)) * ((q - 1) ^ (i + j + 1) * q ^ (T - (i + j + 1) - 1))) + * (deg ^ (T + 1) * q ^ T) + = (deg ^ (T - i) * ((q - 1) ^ i * q ^ (T - i - 1))) + * (deg ^ (T - j) * ((q - 1) ^ j * q ^ (T - j - 1))) * (q - 1) := by + have e1 : deg ^ (T - (i + j + 1)) * deg ^ (T + 1) = deg ^ (T - i) * deg ^ (T - j) := by + rw [← pow_add, ← pow_add] + congr 1 + omega + have e2 : q ^ (T - (i + j + 1) - 1) * q ^ T = q ^ (T - i - 1) * q ^ (T - j - 1) := by + rw [← pow_add, ← pow_add] + congr 1 + omega + have e3 : (q - 1) ^ (i + j + 1) = (q - 1) ^ i * (q - 1) ^ j * (q - 1) := by + rw [pow_add, pow_add, pow_one] + calc (deg ^ (T - (i + j + 1)) * ((q - 1) ^ (i + j + 1) * q ^ (T - (i + j + 1) - 1))) + * (deg ^ (T + 1) * q ^ T) + = (deg ^ (T - (i + j + 1)) * deg ^ (T + 1)) * (q - 1) ^ (i + j + 1) + * (q ^ (T - (i + j + 1) - 1) * q ^ T) := by ring + _ = (deg ^ (T - i) * deg ^ (T - j)) * ((q - 1) ^ i * (q - 1) ^ j * (q - 1)) + * (q ^ (T - i - 1) * q ^ (T - j - 1)) := by rw [e1, e2, e3] + _ = (deg ^ (T - i) * ((q - 1) ^ i * q ^ (T - i - 1))) + * (deg ^ (T - j) * ((q - 1) ^ j * q ^ (T - j - 1))) * (q - 1) := by ring + +/-- **The double sum factorises.** Over a square of positions small enough that +`i + j + 1` never reaches the truncation `T`, the crossing sum is — up to the +normaliser — `(q-1)` times the product of the two weighted sums that the +plurality bound controls. + +Restricting to a square rather than the full triangle `i + j + 1 < T` is what +makes this an identity: the triangle is not a product region. The tail thrown +away is geometrically small. -/ +theorem sum_crossing_factor (deg q T H : ℕ) (hH : 2 * H + 1 < T) (f g : ℕ → ℕ) : + (∑ i ∈ Finset.range (H + 1), ∑ j ∈ Finset.range (H + 1), + f i * g j * (deg ^ (T - (i + j + 1)) + * ((q - 1) ^ (i + j + 1) * q ^ (T - (i + j + 1) - 1)))) + * (deg ^ (T + 1) * q ^ T) + = (q - 1) * ((∑ i ∈ Finset.range (H + 1), + f i * (deg ^ (T - i) * ((q - 1) ^ i * q ^ (T - i - 1)))) + * (∑ j ∈ Finset.range (H + 1), + g j * (deg ^ (T - j) * ((q - 1) ^ j * q ^ (T - j - 1))))) := by + have key : ∀ i ∈ Finset.range (H + 1), ∀ j ∈ Finset.range (H + 1), + (f i * g j * (deg ^ (T - (i + j + 1)) + * ((q - 1) ^ (i + j + 1) * q ^ (T - (i + j + 1) - 1)))) * (deg ^ (T + 1) * q ^ T) + = (q - 1) * ((f i * (deg ^ (T - i) * ((q - 1) ^ i * q ^ (T - i - 1)))) + * (g j * (deg ^ (T - j) * ((q - 1) ^ j * q ^ (T - j - 1))))) := by + intro i hi j hj + simp only [Finset.mem_range] at hi hj + have hij : i + j + 1 < T := by omega + have hw := weight_factor deg q hij + calc (f i * g j * (deg ^ (T - (i + j + 1)) + * ((q - 1) ^ (i + j + 1) * q ^ (T - (i + j + 1) - 1)))) * (deg ^ (T + 1) * q ^ T) + = f i * g j * ((deg ^ (T - (i + j + 1)) + * ((q - 1) ^ (i + j + 1) * q ^ (T - (i + j + 1) - 1))) + * (deg ^ (T + 1) * q ^ T)) := by ring + _ = f i * g j * ((deg ^ (T - i) * ((q - 1) ^ i * q ^ (T - i - 1))) + * (deg ^ (T - j) * ((q - 1) ^ j * q ^ (T - j - 1))) * (q - 1)) := by rw [hw] + _ = (q - 1) * ((f i * (deg ^ (T - i) * ((q - 1) ^ i * q ^ (T - i - 1)))) + * (g j * (deg ^ (T - j) * ((q - 1) ^ j * q ^ (T - j - 1))))) := by ring + calc (∑ i ∈ Finset.range (H + 1), ∑ j ∈ Finset.range (H + 1), + f i * g j * (deg ^ (T - (i + j + 1)) + * ((q - 1) ^ (i + j + 1) * q ^ (T - (i + j + 1) - 1)))) + * (deg ^ (T + 1) * q ^ T) + = ∑ i ∈ Finset.range (H + 1), ∑ j ∈ Finset.range (H + 1), + ((f i * g j * (deg ^ (T - (i + j + 1)) + * ((q - 1) ^ (i + j + 1) * q ^ (T - (i + j + 1) - 1)))) + * (deg ^ (T + 1) * q ^ T)) := by + rw [Finset.sum_mul] + exact Finset.sum_congr rfl fun i _ => Finset.sum_mul _ _ _ + _ = ∑ i ∈ Finset.range (H + 1), ∑ j ∈ Finset.range (H + 1), + ((q - 1) * ((f i * (deg ^ (T - i) * ((q - 1) ^ i * q ^ (T - i - 1)))) + * (g j * (deg ^ (T - j) * ((q - 1) ^ j * q ^ (T - j - 1)))))) := + Finset.sum_congr rfl fun i hi => Finset.sum_congr rfl fun j hj => key i hi j hj + _ = (q - 1) * ((∑ i ∈ Finset.range (H + 1), + f i * (deg ^ (T - i) * ((q - 1) ^ i * q ^ (T - i - 1)))) + * (∑ j ∈ Finset.range (H + 1), + g j * (deg ^ (T - j) * ((q - 1) ^ j * q ^ (T - j - 1))))) := by + rw [Finset.sum_mul_sum, Finset.mul_sum] + refine Finset.sum_congr rfl fun i _ => ?_ + rw [Finset.mul_sum] + +/-- **The geometric tail.** The weights decay geometrically, so the positions +beyond `k` carry at most `(q-1)^k * q^(T-k)` — a `((q-1)/q)^k` fraction of the +total. This is what makes both the truncation term and the restriction to a +square of positions harmless. -/ +theorem geom_tail_le {q : ℕ} (hq : 0 < q) (T : ℕ) : ∀ (n k : ℕ), T - k = n → k ≤ T → + ∑ m ∈ Finset.Ico k T, (q - 1) ^ m * q ^ (T - 1 - m) ≤ (q - 1) ^ k * q ^ (T - k) := by + intro n + induction n with + | zero => + intro k hk hkT + have hkeq : k = T := by omega + subst hkeq + simp + | succ n ih => + intro k hk hkT + have hklt : k < T := by omega + have hrec := ih (k + 1) (by omega) (by omega) + rw [Finset.sum_eq_sum_Ico_succ_bot hklt] + have hstep : (q - 1) ^ k * q ^ (T - 1 - k) + (q - 1) ^ (k + 1) * q ^ (T - (k + 1)) + = (q - 1) ^ k * q ^ (T - k) := by + have hq1 : 1 + (q - 1) = q := by omega + have hidx : T - k = (T - 1 - k) + 1 := by omega + have hidx2 : T - (k + 1) = T - 1 - k := by omega + rw [hidx, hidx2, pow_succ, pow_succ] + calc (q - 1) ^ k * q ^ (T - 1 - k) + (q - 1) ^ k * (q - 1) * q ^ (T - 1 - k) + = (q - 1) ^ k * q ^ (T - 1 - k) * (1 + (q - 1)) := by ring + _ = (q - 1) ^ k * (q ^ (T - 1 - k) * q) := by rw [hq1]; ring + calc (q - 1) ^ k * q ^ (T - 1 - k) + ∑ m ∈ Finset.Ico (k + 1) T, + (q - 1) ^ m * q ^ (T - 1 - m) + ≤ (q - 1) ^ k * q ^ (T - 1 - k) + (q - 1) ^ (k + 1) * q ^ (T - (k + 1)) := + Nat.add_le_add_left hrec _ + _ = (q - 1) ^ k * q ^ (T - k) := hstep + +/-- `sum_crossing_factor` in the form callers can use: the counts are supplied +as an abstract function `C`, since the concrete count at position `(i, j)` is a +`Finset.card` whose very statement needs `i + j + 1 < T`, a fact only available +pointwise inside the sum. -/ +theorem sum_goodCount_factor (deg q T H : ℕ) (hH : 2 * H + 1 < T) (f g : ℕ → ℕ) + (C : ℕ → ℕ → ℕ) + (hC : ∀ i ∈ Finset.range (H + 1), ∀ j ∈ Finset.range (H + 1), + C i j = f i * g j * (deg ^ (T - (i + j + 1)) + * ((q - 1) ^ (i + j + 1) * q ^ (T - (i + j + 1) - 1)))) : + (∑ i ∈ Finset.range (H + 1), ∑ j ∈ Finset.range (H + 1), C i j) + * (deg ^ (T + 1) * q ^ T) + = (q - 1) * ((∑ i ∈ Finset.range (H + 1), + f i * (deg ^ (T - i) * ((q - 1) ^ i * q ^ (T - i - 1)))) + * (∑ j ∈ Finset.range (H + 1), + g j * (deg ^ (T - j) * ((q - 1) ^ j * q ^ (T - j - 1))))) := by + rw [Finset.sum_congr rfl fun i hi => Finset.sum_congr rfl fun j hj => hC i hi j hj] + exact sum_crossing_factor deg q T H hH f g + +namespace RegCSP + +variable {α : Type} [Fintype α] [DecidableEq α] [Nonempty α] +variable (R : RegCSP α) (q T : ℕ) (hq : 0 < q) + +omit [Fintype α] [DecidableEq α] [Nonempty α] in +/-- The prefix condition counted above is exactly the constraint's own +`startIdx` term: the opinion `v` holds about the walk's `i`-th vertex. -/ +theorem opinionOf_eq_startIdx (A : (R.killedPow q T hq).Assignment) {ℓ : ℕ} + (hℓ : ℓ ≤ T) (v : R.graph.V) (W : Fin ℓ → R.graph.D) (i : Fin ℓ) + (hiT : i.val ≤ T) : + R.opinionOf q T hq A (R.graph.walkAt ℓ v W i.val) hiT + (R.graph.revWalk v (R.graph.segPre W (le_of_lt i.isLt))) + = A v (R.graph.startIdx hℓ W i) := by + have hP : R.graph.walkEnd i.val v (R.graph.segPre W (le_of_lt i.isLt)) + = R.graph.walkAt ℓ v W i.val := + (R.graph.walkAt_eq_walkEnd_prefix v W i.val (le_of_lt i.isLt)).symm + rw [opinionOf, ← hP, R.graph.walkEnd_revWalk, R.graph.revWalk_revWalk] + rfl + +omit [Fintype α] [DecidableEq α] [Nonempty α] in +/-- The suffix condition counted above is exactly the constraint's own `endIdx` +term: the opinion the walk's far end holds about the `(i+1)`-st vertex. -/ +theorem opinionOf_eq_endIdx (A : (R.killedPow q T hq).Assignment) {ℓ : ℕ} + (hℓ : ℓ ≤ T) (v : R.graph.V) (W : Fin ℓ → R.graph.D) (i : Fin ℓ) + (hjT : ℓ - (i.val + 1) ≤ T) : + R.opinionOf q T hq A (R.graph.walkAt ℓ v W (i.val + 1)) hjT + (R.graph.segSuf W i.val) + = A (R.graph.walkEnd ℓ v W) (R.graph.endIdx hℓ v W i) := by + have hend : R.graph.walkEnd (ℓ - (i.val + 1)) (R.graph.walkAt ℓ v W (i.val + 1)) + (R.graph.segSuf W i.val) = R.graph.walkEnd ℓ v W := by + have hidx : i.val + 1 + (ℓ - (i.val + 1)) = ℓ := by + have := i.isLt + omega + have h := R.graph.walkAt_segSuf v W i.val (ℓ - (i.val + 1)) (le_refl _) + rw [hidx] at h + simp only [R.graph.walkAt_self_eq_walkEnd] at h + exact h + rw [opinionOf, hend, R.graph.revWalk_segSuf v W i.isLt] + rfl + +/-- **The crossing count with the constraint's own conditions.** For a dart +`(a, d)` of `R` and a crossing position `i` inside an effective length `ℓ`, the +killed-power constraints crossing `(a, d)` at `i` with truthful opinions at both +ends number `truthCount a i * truthCount (nbr a d) (ℓ - (i+1))` times the fibre +weight. -/ +theorem card_good_crossing (A : (R.killedPow q T hq).Assignment) (a : R.graph.V) + (d : R.graph.D) {ℓ i : ℕ} (hℓ : ℓ < T) (hi : i < ℓ) (hiT : i ≤ T) + (hjT : ℓ - (i + 1) ≤ T) : + (Finset.univ.filter fun z : R.graph.V × ((Fin T → R.graph.D) × (Fin T → Fin q)) => + stopAt z.2.2 = ℓ ∧ + (R.graph.walkAt ℓ z.1 (R.graph.preWalk z.2.1 (le_of_lt hℓ)) i = a + ∧ (R.graph.preWalk z.2.1 (le_of_lt hℓ)) ⟨i, hi⟩ = d + ∧ R.opinionOf q T hq A a hiT (R.graph.revWalk z.1 + (R.graph.segPre (R.graph.preWalk z.2.1 (le_of_lt hℓ)) (le_of_lt hi))) + = R.kPlurality q T hq A a + ∧ R.opinionOf q T hq A (R.graph.nbr a d) hjT + (R.graph.segSuf (R.graph.preWalk z.2.1 (le_of_lt hℓ)) i) + = R.kPlurality q T hq A (R.graph.nbr a d))).card + = R.truthCount q T hq A a i + * R.truthCount q T hq A (R.graph.nbr a d) (ℓ - (i + 1)) + * (R.graph.deg ^ (T - ℓ) * ((q - 1) ^ ℓ * q ^ (T - ℓ - 1))) := by + classical + rw [R.graph.card_label_crossing hq hℓ hi a d + (fun p => R.opinionOf q T hq A a hiT p = R.kPlurality q T hq A a) + (fun s => R.opinionOf q T hq A (R.graph.nbr a d) hjT s + = R.kPlurality q T hq A (R.graph.nbr a d)), + truthCount, dif_pos hiT, truthCount, dif_pos hjT] + +/-! ### Every counted crossing breaks its constraint -/ + +/-- **The counted darts really are unsatisfied.** A killed walk crossing a dart +that the decoded assignment fails, with both ends holding the decoded opinion +about that dart's two vertices, breaks its own constraint. The two hypotheses +are exactly the conditions `card_good_crossing` counts, turned into the +constraint's `startIdx` / `endIdx` terms by the two identification lemmas. -/ +theorem not_satisfies_of_good_crossing (A : (R.killedPow q T hq).Assignment) + (z : R.graph.V × R.KLabels q T) (i : Fin (R.graph.kLen z.2)) + (hiT : i.val ≤ T) (hjT : R.graph.kLen z.2 - (i.val + 1) ≤ T) + (hfault : ¬ R.Satisfies (R.kDecode q T hq A) + (R.graph.walkAt (R.graph.kLen z.2) z.1 (R.graph.kWalk z.2) i.val, + R.graph.kWalk z.2 i)) + (hpre : R.opinionOf q T hq A + (R.graph.walkAt (R.graph.kLen z.2) z.1 (R.graph.kWalk z.2) i.val) hiT + (R.graph.revWalk z.1 (R.graph.segPre (R.graph.kWalk z.2) (le_of_lt i.isLt))) + = R.kDecode q T hq A + (R.graph.walkAt (R.graph.kLen z.2) z.1 (R.graph.kWalk z.2) i.val)) + (hsuf : R.opinionOf q T hq A + (R.graph.walkAt (R.graph.kLen z.2) z.1 (R.graph.kWalk z.2) (i.val + 1)) hjT + (R.graph.segSuf (R.graph.kWalk z.2) i.val) + = R.kDecode q T hq A + (R.graph.walkAt (R.graph.kLen z.2) z.1 (R.graph.kWalk z.2) (i.val + 1))) : + ¬ (R.killedPow q T hq).Satisfies A z := by + obtain ⟨v, y⟩ := z + refine R.not_satisfies_killedPow_of_faulty q T hq A v y i hfault ?_ ?_ + · rw [← R.opinionOf_eq_startIdx q T hq A (R.graph.kLen_le y) v (R.graph.kWalk y) i hiT] + exact hpre + · show A (R.graph.walkEnd (R.graph.kLen y) v (R.graph.kWalk y)) + (R.graph.endIdx (R.graph.kLen_le y) v (R.graph.kWalk y) i) = _ + rw [← R.opinionOf_eq_endIdx q T hq A (R.graph.kLen_le y) v (R.graph.kWalk y) i hjT] + exact hsuf + +/-- A walk count never exceeds the number of walks. -/ +theorem truthCount_le (A : (R.killedPow q T hq).Assignment) (v : R.graph.V) (m : ℕ) : + R.truthCount q T hq A v m ≤ R.graph.deg ^ m := by + rw [truthCount] + split + · calc (Finset.univ.filter fun w : Fin m → R.graph.D => + R.opinionOf q T hq A v _ w = R.kPlurality q T hq A v).card + ≤ (Finset.univ : Finset (Fin m → R.graph.D)).card := Finset.card_filter_le _ _ + _ = R.graph.deg ^ m := by + rw [Finset.card_univ, Fintype.card_fun, Fintype.card_fin] + rfl + · exact Nat.zero_le _ + +/-- **Plurality on half the range.** Restricting the weighted sum to positions +at most `H` costs only the geometric tail, so the plurality bound survives the +restriction to a square of positions that `sum_crossing_factor` needs. -/ +theorem card_le_mul_sum_truthCount_half (A : (R.killedPow q T hq).Assignment) + (v : R.graph.V) {H : ℕ} (hH : H + 1 ≤ T) : + R.graph.deg ^ T * q ^ T + ≤ Fintype.card α * ((∑ m ∈ Finset.range (H + 1), + R.truthCount q T hq A v m + * (R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1)))) + + R.graph.deg ^ T * (q - 1) ^ T + + R.graph.deg ^ T * ((q - 1) ^ (H + 1) * q ^ (T - (H + 1)))) := by + classical + have hfull := R.card_le_mul_sum_truthCount q T hq A v + have hsum : ∑ m ∈ Finset.range T, R.truthCount q T hq A v m + * (R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1))) + = (∑ m ∈ Finset.range (H + 1), R.truthCount q T hq A v m + * (R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1)))) + + ∑ m ∈ Finset.Ico (H + 1) T, R.truthCount q T hq A v m + * (R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1))) := by + rw [Finset.range_eq_Ico, Finset.range_eq_Ico, + ← Finset.sum_Ico_consecutive _ (Nat.zero_le (H + 1)) hH] + have hterm : ∀ m ∈ Finset.Ico (H + 1) T, + R.truthCount q T hq A v m + * (R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1))) + ≤ R.graph.deg ^ T * ((q - 1) ^ m * q ^ (T - 1 - m)) := by + intro m hm + simp only [Finset.mem_Ico] at hm + have h1 : R.truthCount q T hq A v m ≤ R.graph.deg ^ m := R.truthCount_le q T hq A v m + have h2 : R.graph.deg ^ m * R.graph.deg ^ (T - m) = R.graph.deg ^ T := by + rw [← pow_add] + congr 1 + omega + have h3 : T - m - 1 = T - 1 - m := by omega + calc R.truthCount q T hq A v m + * (R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1))) + ≤ R.graph.deg ^ m + * (R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1))) := + Nat.mul_le_mul_right _ h1 + _ = R.graph.deg ^ T * ((q - 1) ^ m * q ^ (T - 1 - m)) := by + rw [h3, ← mul_assoc, h2] + have htail : ∑ m ∈ Finset.Ico (H + 1) T, R.truthCount q T hq A v m + * (R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1))) + ≤ R.graph.deg ^ T * ((q - 1) ^ (H + 1) * q ^ (T - (H + 1))) := by + calc ∑ m ∈ Finset.Ico (H + 1) T, R.truthCount q T hq A v m + * (R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1))) + ≤ ∑ m ∈ Finset.Ico (H + 1) T, R.graph.deg ^ T * ((q - 1) ^ m * q ^ (T - 1 - m)) := + Finset.sum_le_sum hterm + _ = R.graph.deg ^ T * ∑ m ∈ Finset.Ico (H + 1) T, (q - 1) ^ m * q ^ (T - 1 - m) := by + rw [Finset.mul_sum] + _ ≤ R.graph.deg ^ T * ((q - 1) ^ (H + 1) * q ^ (T - (H + 1))) := + Nat.mul_le_mul_left _ (geom_tail_le hq T (T - (H + 1)) (H + 1) rfl hH) + rw [hsum] at hfull + refine le_trans hfull (Nat.mul_le_mul_left _ ?_) + omega + +/-- The crossing count indexed by the two *lengths* rather than by length and +position — the form `sum_goodCount_factor` consumes. -/ +theorem card_good_crossing_sq (A : (R.killedPow q T hq).Assignment) (a : R.graph.V) + (d : R.graph.D) {i j : ℕ} (hij : i + j + 1 < T) (hiT : i ≤ T) + (hjT : (i + j + 1) - (i + 1) ≤ T) : + (Finset.univ.filter fun z : R.graph.V × ((Fin T → R.graph.D) × (Fin T → Fin q)) => + stopAt z.2.2 = i + j + 1 ∧ + (R.graph.walkAt (i + j + 1) z.1 (R.graph.preWalk z.2.1 (le_of_lt hij)) i = a + ∧ (R.graph.preWalk z.2.1 (le_of_lt hij)) ⟨i, by omega⟩ = d + ∧ R.opinionOf q T hq A a hiT (R.graph.revWalk z.1 + (R.graph.segPre (R.graph.preWalk z.2.1 (le_of_lt hij)) (by omega))) + = R.kPlurality q T hq A a + ∧ R.opinionOf q T hq A (R.graph.nbr a d) hjT + (R.graph.segSuf (R.graph.preWalk z.2.1 (le_of_lt hij)) i) + = R.kPlurality q T hq A (R.graph.nbr a d))).card + = R.truthCount q T hq A a i * R.truthCount q T hq A (R.graph.nbr a d) j + * (R.graph.deg ^ (T - (i + j + 1)) + * ((q - 1) ^ (i + j + 1) * q ^ (T - (i + j + 1) - 1))) := by + have hsub : (i + j + 1) - (i + 1) = j := by omega + rw [R.card_good_crossing q T hq A a d hij (by omega) hiT hjT, hsub] + +/-! ### The per-dart bound -/ + +/-- The weighted count of truthful walks out of `v`, over positions at most +`H`. -/ +noncomputable def halfSum (A : (R.killedPow q T hq).Assignment) (v : R.graph.V) + (H : ℕ) : ℕ := + ∑ m ∈ Finset.range (H + 1), R.truthCount q T hq A v m + * (R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1))) + +/-- What plurality pays for truncation: the walks that never stop, and those +that stop after position `H`. Both are geometrically small next to +`deg ^ T * q ^ T`. -/ +def pluralityLoss (deg q T H : ℕ) : ℕ := + deg ^ T * (q - 1) ^ T + deg ^ T * ((q - 1) ^ (H + 1) * q ^ (T - (H + 1))) + +theorem card_le_mul_halfSum_add_loss (A : (R.killedPow q T hq).Assignment) + (v : R.graph.V) {H : ℕ} (hH : H + 1 ≤ T) : + R.graph.deg ^ T * q ^ T + ≤ Fintype.card α * R.halfSum q T hq A v H + + Fintype.card α * pluralityLoss R.graph.deg q T H := by + have h := R.card_le_mul_sum_truthCount_half q T hq A v hH + calc R.graph.deg ^ T * q ^ T + ≤ Fintype.card α * ((∑ m ∈ Finset.range (H + 1), R.truthCount q T hq A v m + * (R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1)))) + + R.graph.deg ^ T * (q - 1) ^ T + + R.graph.deg ^ T * ((q - 1) ^ (H + 1) * q ^ (T - (H + 1)))) := h + _ = Fintype.card α * R.halfSum q T hq A v H + + Fintype.card α * pluralityLoss R.graph.deg q T H := by + rw [halfSum, pluralityLoss] + ring + +/-- **The per-dart first moment.** For one dart of `R`, the killed-power +constraints crossing it with truthful opinions at both ends number at least + +`(q-1) · (X - |α|·loss)² / (|α|² · normaliser)`, + +with `X = deg^T · q^T`. The two plurality bounds multiply because the crossing +sum factorises — the payoff of the killed walk law. -/ +theorem per_dart_lower (A : (R.killedPow q T hq).Assignment) (a : R.graph.V) + (d : R.graph.D) {H : ℕ} (hH : 2 * H + 1 < T) (hHT : H + 1 ≤ T) + (C : ℕ → ℕ → ℕ) + (hC : ∀ i ∈ Finset.range (H + 1), ∀ j ∈ Finset.range (H + 1), + C i j = R.truthCount q T hq A a i * R.truthCount q T hq A (R.graph.nbr a d) j + * (R.graph.deg ^ (T - (i + j + 1)) + * ((q - 1) ^ (i + j + 1) * q ^ (T - (i + j + 1) - 1)))) : + (q - 1) * ((R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H) + * (R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H)) + ≤ Fintype.card α ^ 2 + * ((∑ i ∈ Finset.range (H + 1), ∑ j ∈ Finset.range (H + 1), C i j) + * (R.graph.deg ^ (T + 1) * q ^ T)) := by + have hid := sum_goodCount_factor R.graph.deg q T H hH + (fun i => R.truthCount q T hq A a i) + (fun j => R.truthCount q T hq A (R.graph.nbr a d) j) C hC + have ha := R.card_le_mul_halfSum_add_loss q T hq A a hHT + have hb := R.card_le_mul_halfSum_add_loss q T hq A (R.graph.nbr a d) hHT + have ha' : R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H + ≤ Fintype.card α * R.halfSum q T hq A a H := by omega + have hb' : R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H + ≤ Fintype.card α * R.halfSum q T hq A (R.graph.nbr a d) H := by omega + calc (q - 1) * ((R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H) + * (R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H)) + ≤ (q - 1) * ((Fintype.card α * R.halfSum q T hq A a H) + * (Fintype.card α * R.halfSum q T hq A (R.graph.nbr a d) H)) := + Nat.mul_le_mul_left _ (Nat.mul_le_mul ha' hb') + _ = Fintype.card α ^ 2 * ((q - 1) * (R.halfSum q T hq A a H + * R.halfSum q T hq A (R.graph.nbr a d) H)) := by ring + _ = Fintype.card α ^ 2 + * ((∑ i ∈ Finset.range (H + 1), ∑ j ∈ Finset.range (H + 1), C i j) + * (R.graph.deg ^ (T + 1) * q ^ T)) := by + rw [hid, halfSum, halfSum] + +/-- **The first moment, summed over the failed darts.** Every dart of `R` that +the decoded assignment fails contributes its own crossings, and for a given +killed walk and position the dart crossed there is determined, so the +contributions never overlap. -/ +theorem sum_dart_lower (A : (R.killedPow q T hq).Assignment) (F : Finset R.Dart) + {H : ℕ} (hH : 2 * H + 1 < T) (hHT : H + 1 ≤ T) + (Cd : R.Dart → ℕ → ℕ → ℕ) + (hCd : ∀ p ∈ F, ∀ i ∈ Finset.range (H + 1), ∀ j ∈ Finset.range (H + 1), + Cd p i j = R.truthCount q T hq A p.1 i + * R.truthCount q T hq A (R.graph.nbr p.1 p.2) j + * (R.graph.deg ^ (T - (i + j + 1)) + * ((q - 1) ^ (i + j + 1) * q ^ (T - (i + j + 1) - 1)))) : + F.card * ((q - 1) * ((R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H) + * (R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H))) + ≤ Fintype.card α ^ 2 + * ((∑ p ∈ F, ∑ i ∈ Finset.range (H + 1), ∑ j ∈ Finset.range (H + 1), Cd p i j) + * (R.graph.deg ^ (T + 1) * q ^ T)) := by + have hstep : ∀ p ∈ F, (q - 1) * ((R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H) + * (R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H)) + ≤ Fintype.card α ^ 2 + * ((∑ i ∈ Finset.range (H + 1), ∑ j ∈ Finset.range (H + 1), Cd p i j) + * (R.graph.deg ^ (T + 1) * q ^ T)) := fun p hp => + R.per_dart_lower q T hq A p.1 p.2 hH hHT (Cd p) (hCd p hp) + calc F.card * ((q - 1) * ((R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H) + * (R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H))) + = ∑ _p ∈ F, ((q - 1) * ((R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H) + * (R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H))) := by + rw [Finset.sum_const, smul_eq_mul] + _ ≤ ∑ p ∈ F, (Fintype.card α ^ 2 + * ((∑ i ∈ Finset.range (H + 1), ∑ j ∈ Finset.range (H + 1), Cd p i j) + * (R.graph.deg ^ (T + 1) * q ^ T))) := Finset.sum_le_sum hstep + _ = Fintype.card α ^ 2 + * ((∑ p ∈ F, ∑ i ∈ Finset.range (H + 1), ∑ j ∈ Finset.range (H + 1), Cd p i j) + * (R.graph.deg ^ (T + 1) * q ^ T)) := by + rw [← Finset.mul_sum, ← Finset.sum_mul] + +/-! ### The crossing count of a single constraint -/ + +/-- The steps at which a killed walk crosses a dart the decoded assignment +fails, with truthful opinions at both ends. Its cardinality is the count the +second-moment method is applied to. -/ +noncomputable def goodCrossings (A : (R.killedPow q T hq).Assignment) + (z : R.graph.V × R.KLabels q T) : Finset (Fin (R.graph.kLen z.2)) := + Finset.univ.filter fun i => + (¬ R.Satisfies (R.kDecode q T hq A) + (R.graph.walkAt (R.graph.kLen z.2) z.1 (R.graph.kWalk z.2) i.val, + R.graph.kWalk z.2 i)) + ∧ R.opinionOf q T hq A + (R.graph.walkAt (R.graph.kLen z.2) z.1 (R.graph.kWalk z.2) i.val) + (le_trans (le_of_lt i.isLt) (R.graph.kLen_le z.2)) + (R.graph.revWalk z.1 (R.graph.segPre (R.graph.kWalk z.2) (le_of_lt i.isLt))) + = R.kDecode q T hq A + (R.graph.walkAt (R.graph.kLen z.2) z.1 (R.graph.kWalk z.2) i.val) + ∧ R.opinionOf q T hq A + (R.graph.walkAt (R.graph.kLen z.2) z.1 (R.graph.kWalk z.2) (i.val + 1)) + (le_trans (Nat.sub_le _ _) (R.graph.kLen_le z.2)) + (R.graph.segSuf (R.graph.kWalk z.2) i.val) + = R.kDecode q T hq A + (R.graph.walkAt (R.graph.kLen z.2) z.1 (R.graph.kWalk z.2) (i.val + 1)) + +/-- **The support condition.** A constraint with any good crossing is +unsatisfied, so the second-moment method's support sits inside the unsatisfied +darts. -/ +theorem mem_unsatDarts_of_goodCrossings_nonempty (A : (R.killedPow q T hq).Assignment) + (z : R.graph.V × R.KLabels q T) (h : (R.goodCrossings q T hq A z).Nonempty) : + z ∈ (R.killedPow q T hq).unsatDarts A := by + obtain ⟨i, hi⟩ := h + rw [goodCrossings, Finset.mem_filter] at hi + have hns := R.not_satisfies_of_good_crossing q T hq A z i _ _ hi.2.1 hi.2.2.1 hi.2.2.2 + exact (RegCSP.mem_unsatDarts (R := R.killedPow q T hq) (a := A) (p := z)).mpr hns + +/-- **The second moment, in terms of pairs of crossings.** Squaring the count +of good crossings and summing turns into the count itself plus twice the ordered +pairs — and pairs of crossings are what the correlation bound estimates. -/ +theorem sum_sq_goodCrossings (A : (R.killedPow q T hq).Assignment) : + ∑ z : R.graph.V × R.KLabels q T, ((R.goodCrossings q T hq A z).card : ℝ) ^ 2 + = (∑ z : R.graph.V × R.KLabels q T, ((R.goodCrossings q T hq A z).card : ℝ)) + + 2 * ∑ z : R.graph.V × R.KLabels q T, + ((((R.goodCrossings q T hq A z) ×ˢ (R.goodCrossings q T hq A z)).filter + fun p => p.1 < p.2).card : ℝ) := by + classical + have hpt : ∀ z : R.graph.V × R.KLabels q T, + ((R.goodCrossings q T hq A z).card : ℝ) ^ 2 + = ((R.goodCrossings q T hq A z).card : ℝ) + + 2 * ((((R.goodCrossings q T hq A z) ×ˢ (R.goodCrossings q T hq A z)).filter + fun p => p.1 < p.2).card : ℝ) := by + intro z + have h := card_sq_eq_add_two_mul_pairs (R.goodCrossings q T hq A z) + exact_mod_cast congrArg (fun n : ℕ => (n : ℝ)) h + rw [Finset.sum_congr rfl fun z _ => hpt z, Finset.sum_add_distrib, Finset.mul_sum] + +/-! ### Positions as naturals -/ + +/-- The good crossing positions of a constraint, as naturals. The `Fin` version +carries the constraint's own length in its type, which blocks the exchange of +summation order the second moment needs; this one does not. -/ +noncomputable def goodPos (A : (R.killedPow q T hq).Assignment) + (z : R.graph.V × R.KLabels q T) : Finset ℕ := + (R.goodCrossings q T hq A z).image Fin.val + +theorem card_goodPos (A : (R.killedPow q T hq).Assignment) + (z : R.graph.V × R.KLabels q T) : + (R.goodPos q T hq A z).card = (R.goodCrossings q T hq A z).card := + Finset.card_image_of_injective _ Fin.val_injective + +theorem mem_goodPos {A : (R.killedPow q T hq).Assignment} + {z : R.graph.V × R.KLabels q T} {k : ℕ} : + k ∈ R.goodPos q T hq A z + ↔ ∃ h : k < R.graph.kLen z.2, (⟨k, h⟩ : Fin (R.graph.kLen z.2)) + ∈ R.goodCrossings q T hq A z := by + classical + rw [goodPos, Finset.mem_image] + constructor + · rintro ⟨i, hi, rfl⟩ + exact ⟨i.isLt, by simpa using hi⟩ + · rintro ⟨h, hmem⟩ + exact ⟨⟨k, h⟩, hmem, rfl⟩ + +/-- Counting ordered pairs of positions is the same in either indexing. -/ +theorem card_pairs_goodPos (A : (R.killedPow q T hq).Assignment) + (z : R.graph.V × R.KLabels q T) : + (((R.goodPos q T hq A z) ×ˢ (R.goodPos q T hq A z)).filter fun p => p.1 < p.2).card + = (((R.goodCrossings q T hq A z) ×ˢ (R.goodCrossings q T hq A z)).filter + fun p => p.1 < p.2).card := by + classical + refine (Finset.card_bij (fun p _ => ((p.1.val : ℕ), (p.2.val : ℕ))) ?_ ?_ ?_).symm + · intro p hp + simp only [Finset.mem_filter, Finset.mem_product] at hp ⊢ + refine ⟨⟨?_, ?_⟩, hp.2⟩ + · exact Finset.mem_image_of_mem _ hp.1.1 + · exact Finset.mem_image_of_mem _ hp.1.2 + · intro p _ p' _ hpp + have h1 : p.1.val = p'.1.val := congrArg Prod.fst hpp + have h2 : p.2.val = p'.2.val := congrArg Prod.snd hpp + exact Prod.ext (Fin.ext h1) (Fin.ext h2) + · intro p hp + simp only [Finset.mem_filter, Finset.mem_product] at hp + obtain ⟨⟨h1, h2⟩, hlt⟩ := hp + obtain ⟨hb1, hm1⟩ := (R.mem_goodPos q T hq).mp h1 + obtain ⟨hb2, hm2⟩ := (R.mem_goodPos q T hq).mp h2 + refine ⟨(⟨p.1, hb1⟩, ⟨p.2, hb2⟩), ?_, rfl⟩ + simp only [Finset.mem_filter, Finset.mem_product] + exact ⟨⟨hm1, hm2⟩, hlt⟩ + +theorem lt_T_of_mem_goodPos {A : (R.killedPow q T hq).Assignment} + {z : R.graph.V × R.KLabels q T} {k : ℕ} (h : k ∈ R.goodPos q T hq A z) : k < T := by + obtain ⟨hb, -⟩ := (R.mem_goodPos q T hq).mp h + exact lt_of_lt_of_le hb (R.graph.kLen_le z.2) + +/-- **Exchanging the order of counting.** The pairs of good crossings, summed +over the constraints, are the same as the constraints with two good crossings, +summed over the pairs of positions. The right-hand side is the form the +correlation bound estimates. -/ +theorem sum_card_pairs_eq (A : (R.killedPow q T hq).Assignment) : + ∑ z : R.graph.V × R.KLabels q T, + (((R.goodPos q T hq A z) ×ˢ (R.goodPos q T hq A z)).filter fun p => p.1 < p.2).card + = ∑ p ∈ ((Finset.range T) ×ˢ (Finset.range T)).filter fun p => p.1 < p.2, + (Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + p.1 ∈ R.goodPos q T hq A z ∧ p.2 ∈ R.goodPos q T hq A z).card := by + classical + have hzcard : ∀ z : R.graph.V × R.KLabels q T, + (((R.goodPos q T hq A z) ×ˢ (R.goodPos q T hq A z)).filter fun p => p.1 < p.2).card + = ((((Finset.range T) ×ˢ (Finset.range T)).filter fun p => p.1 < p.2).filter + fun p => p.1 ∈ R.goodPos q T hq A z ∧ p.2 ∈ R.goodPos q T hq A z).card := by + intro z + congr 1 + ext p + simp only [Finset.mem_filter, Finset.mem_product, Finset.mem_range] + constructor + · rintro ⟨⟨h1, h2⟩, hlt⟩ + exact ⟨⟨⟨R.lt_T_of_mem_goodPos q T hq h1, R.lt_T_of_mem_goodPos q T hq h2⟩, hlt⟩, h1, h2⟩ + · rintro ⟨⟨-, hlt⟩, h1, h2⟩ + exact ⟨⟨h1, h2⟩, hlt⟩ + rw [Finset.sum_congr rfl fun z _ => hzcard z] + simp only [Finset.card_filter] + exact Finset.sum_comm + +/-- **Dropping to the underlying walk.** Constraints with good crossings at two +positions are, after forgetting the truthfulness conditions and the stopping +signals, walks that cross a failed dart at both positions. Forgetting the +signals costs the factor `q ^ T`; both omissions only weaken an upper bound. -/ +theorem card_both_good_le (A : (R.killedPow q T hq).Assignment) {k l : ℕ} + (hk : k < T) (hl : l < T) : + (Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + k ∈ R.goodPos q T hq A z ∧ l ∈ R.goodPos q T hq A z).card + ≤ q ^ T * (Finset.univ.filter fun w : R.graph.V × (Fin T → R.graph.D) => + (R.graph.walkAt T w.1 w.2 k, w.2 ⟨k, hk⟩) ∈ R.unsatDarts (R.kDecode q T hq A) + ∧ (R.graph.walkAt T w.1 w.2 l, w.2 ⟨l, hl⟩) + ∈ R.unsatDarts (R.kDecode q T hq A)).card := by + classical + have hfaulty : ∀ (z : R.graph.V × R.KLabels q T) (j : ℕ) (hj : j < T), + j ∈ R.goodPos q T hq A z → + (R.graph.walkAt T z.1 z.2.1 j, z.2.1 ⟨j, hj⟩) + ∈ R.unsatDarts (R.kDecode q T hq A) := by + intro z j hj hmem + obtain ⟨hb, hgc⟩ := (R.mem_goodPos q T hq).mp hmem + rw [goodCrossings, Finset.mem_filter] at hgc + have hns := hgc.2.1 + have hwalk : R.graph.walkAt (R.graph.kLen z.2) z.1 (R.graph.kWalk z.2) j + = R.graph.walkAt T z.1 z.2.1 j := + R.graph.walkAt_preWalk (R.graph.kLen_le z.2) z.1 z.2.1 j (le_of_lt hb) + have hlab : R.graph.kWalk z.2 ⟨j, hb⟩ = z.2.1 ⟨j, hj⟩ := rfl + rw [hwalk, hlab] at hns + exact (RegCSP.mem_unsatDarts (R := R) (a := R.kDecode q T hq A) + (p := (R.graph.walkAt T z.1 z.2.1 j, z.2.1 ⟨j, hj⟩))).mpr hns + refine le_trans (Finset.card_le_mul_card_image_of_maps_to + (f := fun z : R.graph.V × R.KLabels q T => (z.1, z.2.1)) + (t := Finset.univ.filter fun w : R.graph.V × (Fin T → R.graph.D) => + (R.graph.walkAt T w.1 w.2 k, w.2 ⟨k, hk⟩) ∈ R.unsatDarts (R.kDecode q T hq A) + ∧ (R.graph.walkAt T w.1 w.2 l, w.2 ⟨l, hl⟩) + ∈ R.unsatDarts (R.kDecode q T hq A)) ?_ (q ^ T) ?_) (le_refl _) + · intro z hz + simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hz ⊢ + exact ⟨hfaulty z k hk hz.1, hfaulty z l hl hz.2⟩ + · intro w _ + refine le_trans (Finset.card_le_card_of_injOn (fun z => z.2.2) + (fun _ _ => Finset.mem_univ _) ?_) ?_ + · intro z hz z' hz' hzz + simp only [Finset.coe_filter, Set.mem_setOf_eq] at hz hz' + have hz1 : z.1 = w.1 := congrArg Prod.fst hz.2 + have hz2 : z.2.1 = w.2 := congrArg Prod.snd hz.2 + have hz1' : z'.1 = w.1 := congrArg Prod.fst hz'.2 + have hz2' : z'.2.1 = w.2 := congrArg Prod.snd hz'.2 + exact Prod.ext (hz1.trans hz1'.symm) (Prod.ext (hz2.trans hz2'.symm) hzz) + · rw [Finset.card_univ, Fintype.card_fun, Fintype.card_fin, Fintype.card_fin] + +/-- **The second moment's pair term, bounded.** Chaining the drop to underlying +walks, the two-crossing identity and the correlation bound. -/ +theorem sum_pairs_bound (A : (R.killedPow q T hq).Assignment) {lam : ℝ} + (hlam0 : 0 ≤ lam) (hlam1 : lam < 1) (hspec : R.graph.SpectralBound lam) + (hn : 0 < R.graph.order) : + ∑ p ∈ ((Finset.range T) ×ˢ (Finset.range T)).filter fun p => p.1 < p.2, + ((Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + p.1 ∈ R.goodPos q T hq A z ∧ p.2 ∈ R.goodPos q T hq A z).card : ℝ) + ≤ (q : ℝ) ^ T * ((R.graph.deg : ℝ) ^ (T - 2) + * ((T : ℝ) * (T : ℝ) * (((R.unsatDarts (R.kDecode q T hq A)).card : ℝ) + * ((R.unsatDarts (R.kDecode q T hq A)).card : ℝ) / (R.graph.order : ℝ)) + + (T : ℝ) * (1 / (1 - lam)) * ((R.graph.deg : ℝ) + * ((R.unsatDarts (R.kDecode q T hq A)).card : ℝ)))) := by + classical + set F := R.unsatDarts (R.kDecode q T hq A) with hF + set Cop : ℕ → ℕ → ℝ := fun k l => ∑ z : R.graph.V, (R.graph.headCount F z : ℝ) + * R.graph.stepIter (l - k - 1) (fun w => (R.graph.dartCount F w : ℝ)) z with hCop + have hterm : ∀ p ∈ ((Finset.range T) ×ˢ (Finset.range T)).filter fun p => p.1 < p.2, + ((Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + p.1 ∈ R.goodPos q T hq A z ∧ p.2 ∈ R.goodPos q T hq A z).card : ℝ) + ≤ (q : ℝ) ^ T * ((R.graph.deg : ℝ) ^ (T - 2) * Cop p.1 p.2) := by + intro p hp + simp only [Finset.mem_filter, Finset.mem_product, Finset.mem_range] at hp + obtain ⟨⟨hk, hl⟩, hlt⟩ := hp + have h1 := R.card_both_good_le q T hq A hk hl + have h1R : ((Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + p.1 ∈ R.goodPos q T hq A z ∧ p.2 ∈ R.goodPos q T hq A z).card : ℝ) + ≤ (q : ℝ) ^ T * ((Finset.univ.filter fun w : R.graph.V × (Fin T → R.graph.D) => + (R.graph.walkAt T w.1 w.2 p.1, w.2 ⟨p.1, hk⟩) ∈ F + ∧ (R.graph.walkAt T w.1 w.2 p.2, w.2 ⟨p.2, hl⟩) ∈ F).card : ℝ) := by + exact_mod_cast h1 + refine le_trans h1R ?_ + have hcount : ((Finset.univ.filter fun w : R.graph.V × (Fin T → R.graph.D) => + (R.graph.walkAt T w.1 w.2 p.1, w.2 ⟨p.1, hk⟩) ∈ F + ∧ (R.graph.walkAt T w.1 w.2 p.2, w.2 ⟨p.2, hl⟩) ∈ F).card : ℝ) + = (R.graph.deg : ℝ) ^ (T - 2) * Cop p.1 p.2 := by + rw [card_filter_eq_sum_prod, Fintype.sum_prod_type, hCop] + exact R.graph.sum_two_crossings F hlt hl + rw [hcount] + refine le_trans (Finset.sum_le_sum hterm) ?_ + have hfactor : ∑ p ∈ ((Finset.range T) ×ˢ (Finset.range T)).filter fun p => p.1 < p.2, + (q : ℝ) ^ T * ((R.graph.deg : ℝ) ^ (T - 2) * Cop p.1 p.2) + = (q : ℝ) ^ T * ((R.graph.deg : ℝ) ^ (T - 2) + * ∑ k ∈ Finset.range T, ∑ l ∈ Finset.Ico (k + 1) T, Cop k l) := by + rw [← sum_pairs_eq_sum_Ico T fun p => Cop p.1 p.2, Finset.mul_sum, Finset.mul_sum] + rw [hfactor] + have hnn : (0 : ℝ) ≤ (q : ℝ) ^ T * (R.graph.deg : ℝ) ^ (T - 2) := by positivity + have hinner := R.graph.sum_pairs_le F hlam0 hlam1 hspec hn T Cop + (fun k _ l _ => rfl) + calc (q : ℝ) ^ T * ((R.graph.deg : ℝ) ^ (T - 2) + * ∑ k ∈ Finset.range T, ∑ l ∈ Finset.Ico (k + 1) T, Cop k l) + = ((q : ℝ) ^ T * (R.graph.deg : ℝ) ^ (T - 2)) + * ∑ k ∈ Finset.range T, ∑ l ∈ Finset.Ico (k + 1) T, Cop k l := by ring + _ ≤ ((q : ℝ) ^ T * (R.graph.deg : ℝ) ^ (T - 2)) + * ((T : ℝ) * (T : ℝ) * ((F.card : ℝ) * (F.card : ℝ) / (R.graph.order : ℝ)) + + (T : ℝ) * (1 / (1 - lam)) * ((R.graph.deg : ℝ) * (F.card : ℝ))) := + mul_le_mul_of_nonneg_left hinner hnn + _ = (q : ℝ) ^ T * ((R.graph.deg : ℝ) ^ (T - 2) + * ((T : ℝ) * (T : ℝ) * ((F.card : ℝ) * (F.card : ℝ) / (R.graph.order : ℝ)) + + (T : ℝ) * (1 / (1 - lam)) * ((R.graph.deg : ℝ) * (F.card : ℝ)))) := by ring + +/-- **The second moment of the crossing count.** Everything above, assembled: +the sum of squares is the sum plus twice a pair term, and the pair term is +controlled by the spectral gap. -/ +theorem sum_sq_goodCrossings_le (A : (R.killedPow q T hq).Assignment) {lam : ℝ} + (hlam0 : 0 ≤ lam) (hlam1 : lam < 1) (hspec : R.graph.SpectralBound lam) + (hn : 0 < R.graph.order) : + ∑ z : R.graph.V × R.KLabels q T, ((R.goodCrossings q T hq A z).card : ℝ) ^ 2 + ≤ (∑ z : R.graph.V × R.KLabels q T, ((R.goodCrossings q T hq A z).card : ℝ)) + + 2 * ((q : ℝ) ^ T * ((R.graph.deg : ℝ) ^ (T - 2) + * ((T : ℝ) * (T : ℝ) * (((R.unsatDarts (R.kDecode q T hq A)).card : ℝ) + * ((R.unsatDarts (R.kDecode q T hq A)).card : ℝ) / (R.graph.order : ℝ)) + + (T : ℝ) * (1 / (1 - lam)) * ((R.graph.deg : ℝ) + * ((R.unsatDarts (R.kDecode q T hq A)).card : ℝ))))) := by + classical + rw [R.sum_sq_goodCrossings q T hq A] + have hpair : ∑ z : R.graph.V × R.KLabels q T, + ((((R.goodCrossings q T hq A z) ×ˢ (R.goodCrossings q T hq A z)).filter + fun p => p.1 < p.2).card : ℝ) + = ∑ p ∈ ((Finset.range T) ×ˢ (Finset.range T)).filter fun p => p.1 < p.2, + ((Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + p.1 ∈ R.goodPos q T hq A z ∧ p.2 ∈ R.goodPos q T hq A z).card : ℝ) := by + rw [← Nat.cast_sum, ← Nat.cast_sum] + congr 1 + rw [Finset.sum_congr rfl fun z _ => (R.card_pairs_goodPos q T hq A z).symm] + exact R.sum_card_pairs_eq q T hq A + rw [hpair] + have hbound := R.sum_pairs_bound q T hq A hlam0 hlam1 hspec hn + linarith [hbound] + +/-- The first moment, with the order of counting exchanged: summing the good +crossings over the constraints is the same as counting, for each position, the +constraints good there. -/ +theorem sum_card_goodPos_eq (A : (R.killedPow q T hq).Assignment) : + ∑ z : R.graph.V × R.KLabels q T, (R.goodPos q T hq A z).card + = ∑ k ∈ Finset.range T, + (Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + k ∈ R.goodPos q T hq A z).card := by + classical + have hzcard : ∀ z : R.graph.V × R.KLabels q T, + (R.goodPos q T hq A z).card + = ((Finset.range T).filter fun k => k ∈ R.goodPos q T hq A z).card := by + intro z + congr 1 + ext k + simp only [Finset.mem_filter, Finset.mem_range] + exact ⟨fun h => ⟨R.lt_T_of_mem_goodPos q T hq h, h⟩, fun h => h.2⟩ + rw [Finset.sum_congr rfl fun z _ => hzcard z] + simp only [Finset.card_filter] + exact Finset.sum_comm + +/-- Constraints of different effective lengths are different constraints, so +summing over the suffix length `j` at a fixed crossing position stays within the +constraints good at that position. -/ +theorem sum_over_len_le (A : (R.killedPow q T hq).Assignment) (i H : ℕ) : + ∑ j ∈ Finset.range (H + 1), + (Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + stopAt z.2.2 = i + j + 1 ∧ i ∈ R.goodPos q T hq A z).card + ≤ (Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + i ∈ R.goodPos q T hq A z).card := by + classical + have hdisj : ∀ j ∈ Finset.range (H + 1), ∀ j' ∈ Finset.range (H + 1), j ≠ j' → + Disjoint + (Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + stopAt z.2.2 = i + j + 1 ∧ i ∈ R.goodPos q T hq A z) + (Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + stopAt z.2.2 = i + j' + 1 ∧ i ∈ R.goodPos q T hq A z) := by + intro j _ j' _ hjj + refine Finset.disjoint_left.mpr fun z hz hz' => ?_ + simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hz hz' + apply hjj + have := hz.1.symm.trans hz'.1 + omega + rw [← Finset.card_biUnion hdisj] + refine Finset.card_le_card ?_ + intro z hz + simp only [Finset.mem_biUnion, Finset.mem_filter, Finset.mem_univ, true_and] at hz ⊢ + obtain ⟨j, -, -, hgood⟩ := hz + exact hgood + +/-- **From a counted crossing to a good position.** A constraint counted by +`card_good_crossing_sq`, whose crossed dart the decoded assignment fails, is good +at that position. The two descriptions differ only in how the walk's length is +named — `i + j + 1` on one side, `kLen` on the other — which `walkAt_preWalk` +and `opinionOf_congr` reconcile without any transport. -/ +theorem mem_goodPos_of_crossing (A : (R.killedPow q T hq).Assignment) (a : R.graph.V) + (d : R.graph.D) {i j : ℕ} (hij : i + j + 1 < T) (hiT : i ≤ T) + (hjT : (i + j + 1) - (i + 1) ≤ T) + (hfault : ¬ R.Satisfies (R.kDecode q T hq A) (a, d)) + (z : R.graph.V × R.KLabels q T) (hlen : stopAt z.2.2 = i + j + 1) + (hwalk : R.graph.walkAt (i + j + 1) z.1 (R.graph.preWalk z.2.1 (le_of_lt hij)) i = a) + (hlab : R.graph.preWalk z.2.1 (le_of_lt hij) ⟨i, by omega⟩ = d) + (hpre : R.opinionOf q T hq A a hiT (R.graph.revWalk z.1 + (R.graph.segPre (R.graph.preWalk z.2.1 (le_of_lt hij)) (by omega))) + = R.kPlurality q T hq A a) + (hsuf : R.opinionOf q T hq A (R.graph.nbr a d) hjT + (R.graph.segSuf (R.graph.preWalk z.2.1 (le_of_lt hij)) i) + = R.kPlurality q T hq A (R.graph.nbr a d)) : + i ∈ R.goodPos q T hq A z := by + classical + have hkl : R.graph.kLen z.2 = i + j + 1 := hlen + have hb : i < R.graph.kLen z.2 := by omega + refine (R.mem_goodPos q T hq).mpr ⟨hb, ?_⟩ + -- the walk position, computed two ways + have hA1 : R.graph.walkAt (R.graph.kLen z.2) z.1 (R.graph.kWalk z.2) i + = R.graph.walkAt T z.1 z.2.1 i := + R.graph.walkAt_preWalk (R.graph.kLen_le z.2) z.1 z.2.1 i (le_of_lt hb) + have hA2 : R.graph.walkAt (i + j + 1) z.1 (R.graph.preWalk z.2.1 (le_of_lt hij)) i + = R.graph.walkAt T z.1 z.2.1 i := + R.graph.walkAt_preWalk (le_of_lt hij) z.1 z.2.1 i (by omega) + have hvert : R.graph.walkAt (R.graph.kLen z.2) z.1 (R.graph.kWalk z.2) i = a := by + rw [hA1, ← hA2, hwalk] + have hlabel : R.graph.kWalk z.2 ⟨i, hb⟩ = d := hlab + -- the prefix walk, pointwise equal on the nose + have hprewalk : R.graph.segPre (R.graph.kWalk z.2) (le_of_lt hb) + = R.graph.segPre (R.graph.preWalk z.2.1 (le_of_lt hij)) (by omega) := rfl + rw [goodCrossings, Finset.mem_filter] + refine ⟨Finset.mem_univ _, ?_, ?_, ?_⟩ + · rw [hvert, hlabel] + exact hfault + · rw [hvert, hprewalk] + exact hpre + · have hnext : R.graph.walkAt (R.graph.kLen z.2) z.1 (R.graph.kWalk z.2) (i + 1) + = R.graph.nbr a d := by + rw [R.graph.walkAt_succ_of_lt z.1 (R.graph.kWalk z.2) hb, hvert, hlabel] + rw [hnext] + refine Eq.trans ?_ hsuf + have hsub : R.graph.kLen z.2 - ((⟨i, hb⟩ : Fin (R.graph.kLen z.2)).val + 1) + = (i + j + 1) - (i + 1) := by + dsimp only + omega + refine R.opinionOf_congr q T hq A (R.graph.nbr a d) _ hjT hsub _ _ ?_ + intro k hk hk' + rfl + +/-- The constraints counted by `card_good_crossing_sq`, packaged. -/ +noncomputable def crossingSet (A : (R.killedPow q T hq).Assignment) (a : R.graph.V) + (d : R.graph.D) {i j : ℕ} (hij : i + j + 1 < T) (hiT : i ≤ T) + (hjT : (i + j + 1) - (i + 1) ≤ T) : Finset (R.graph.V × R.KLabels q T) := + Finset.univ.filter fun z => + stopAt z.2.2 = i + j + 1 ∧ + (R.graph.walkAt (i + j + 1) z.1 (R.graph.preWalk z.2.1 (le_of_lt hij)) i = a + ∧ (R.graph.preWalk z.2.1 (le_of_lt hij)) ⟨i, by omega⟩ = d + ∧ R.opinionOf q T hq A a hiT (R.graph.revWalk z.1 + (R.graph.segPre (R.graph.preWalk z.2.1 (le_of_lt hij)) (by omega))) + = R.kPlurality q T hq A a + ∧ R.opinionOf q T hq A (R.graph.nbr a d) hjT + (R.graph.segSuf (R.graph.preWalk z.2.1 (le_of_lt hij)) i) + = R.kPlurality q T hq A (R.graph.nbr a d)) + +theorem card_crossingSet (A : (R.killedPow q T hq).Assignment) (a : R.graph.V) + (d : R.graph.D) {i j : ℕ} (hij : i + j + 1 < T) (hiT : i ≤ T) + (hjT : (i + j + 1) - (i + 1) ≤ T) : + (R.crossingSet q T hq A a d hij hiT hjT).card + = R.truthCount q T hq A a i * R.truthCount q T hq A (R.graph.nbr a d) j + * (R.graph.deg ^ (T - (i + j + 1)) + * ((q - 1) ^ (i + j + 1) * q ^ (T - (i + j + 1) - 1))) := + R.card_good_crossing_sq q T hq A a d hij hiT hjT + +/-- **The crossings of different darts do not overlap.** At a fixed position the +dart a walk crosses is determined, so summing over the failed darts stays within +the constraints good at that position. -/ +theorem sum_crossingSet_le (A : (R.killedPow q T hq).Assignment) {i j : ℕ} + (hij : i + j + 1 < T) (hiT : i ≤ T) (hjT : (i + j + 1) - (i + 1) ≤ T) : + ∑ p ∈ R.unsatDarts (R.kDecode q T hq A), + (R.crossingSet q T hq A p.1 p.2 hij hiT hjT).card + ≤ (Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + stopAt z.2.2 = i + j + 1 ∧ i ∈ R.goodPos q T hq A z).card := by + classical + have hdisj : ∀ p ∈ R.unsatDarts (R.kDecode q T hq A), + ∀ p' ∈ R.unsatDarts (R.kDecode q T hq A), p ≠ p' → + Disjoint (R.crossingSet q T hq A p.1 p.2 hij hiT hjT) + (R.crossingSet q T hq A p'.1 p'.2 hij hiT hjT) := by + intro p _ p' _ hpp + refine Finset.disjoint_left.mpr fun z hz hz' => ?_ + rw [crossingSet, Finset.mem_filter] at hz hz' + apply hpp + exact Prod.ext (hz.2.2.1.symm.trans hz'.2.2.1) (hz.2.2.2.1.symm.trans hz'.2.2.2.1) + rw [← Finset.card_biUnion hdisj] + refine Finset.card_le_card ?_ + intro z hz + rw [Finset.mem_biUnion] at hz + obtain ⟨p, hp, hzp⟩ := hz + rw [crossingSet, Finset.mem_filter] at hzp + obtain ⟨-, hlen, hwalk, hlab, hpre, hsuf⟩ := hzp + have hfault : ¬ R.Satisfies (R.kDecode q T hq A) (p.1, p.2) := by + have := (RegCSP.mem_unsatDarts (R := R) (a := R.kDecode q T hq A) (p := p)).mp hp + exact this + simp only [Finset.mem_filter, Finset.mem_univ, true_and] + exact ⟨hlen, R.mem_goodPos_of_crossing q T hq A p.1 p.2 hij hiT hjT hfault z hlen hwalk + hlab hpre hsuf⟩ + +/-- Summed over both lengths, the constraints good at a position never exceed +the total crossing count. -/ +theorem sum_len_pos_le (A : (R.killedPow q T hq).Assignment) {H : ℕ} (hHT : H + 1 ≤ T) : + ∑ i ∈ Finset.range (H + 1), ∑ j ∈ Finset.range (H + 1), + (Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + stopAt z.2.2 = i + j + 1 ∧ i ∈ R.goodPos q T hq A z).card + ≤ ∑ z : R.graph.V × R.KLabels q T, (R.goodCrossings q T hq A z).card := by + classical + calc ∑ i ∈ Finset.range (H + 1), ∑ j ∈ Finset.range (H + 1), + (Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + stopAt z.2.2 = i + j + 1 ∧ i ∈ R.goodPos q T hq A z).card + ≤ ∑ i ∈ Finset.range (H + 1), + (Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + i ∈ R.goodPos q T hq A z).card := + Finset.sum_le_sum fun i _ => R.sum_over_len_le q T hq A i H + _ ≤ ∑ k ∈ Finset.range T, + (Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + k ∈ R.goodPos q T hq A z).card := by + refine Finset.sum_le_sum_of_subset ?_ + intro i hi + simp only [Finset.mem_range] at hi ⊢ + omega + _ = ∑ z : R.graph.V × R.KLabels q T, (R.goodPos q T hq A z).card := + (R.sum_card_goodPos_eq q T hq A).symm + _ = ∑ z : R.graph.V × R.KLabels q T, (R.goodCrossings q T hq A z).card := + Finset.sum_congr rfl fun z _ => R.card_goodPos q T hq A z + +/-- **The first moment, bounded by the crossing count.** The counts summed by +`sum_dart_lower` never exceed the total number of good crossings. -/ +theorem sum_Cd_le_sum_goodCrossings (A : (R.killedPow q T hq).Assignment) {H : ℕ} + (hHT : H + 1 ≤ T) (Cd : R.Dart → ℕ → ℕ → ℕ) + (hCd : ∀ p ∈ R.unsatDarts (R.kDecode q T hq A), ∀ i ∈ Finset.range (H + 1), + ∀ j ∈ Finset.range (H + 1), ∀ (hij : i + j + 1 < T) (hiT : i ≤ T) + (hjT : (i + j + 1) - (i + 1) ≤ T), + Cd p i j = (R.crossingSet q T hq A p.1 p.2 hij hiT hjT).card) + (hsq : ∀ i ∈ Finset.range (H + 1), ∀ j ∈ Finset.range (H + 1), i + j + 1 < T) : + ∑ p ∈ R.unsatDarts (R.kDecode q T hq A), ∑ i ∈ Finset.range (H + 1), + ∑ j ∈ Finset.range (H + 1), Cd p i j + ≤ ∑ z : R.graph.V × R.KLabels q T, (R.goodCrossings q T hq A z).card := by + classical + have hreorder : ∑ p ∈ R.unsatDarts (R.kDecode q T hq A), ∑ i ∈ Finset.range (H + 1), + ∑ j ∈ Finset.range (H + 1), Cd p i j + = ∑ i ∈ Finset.range (H + 1), ∑ j ∈ Finset.range (H + 1), + ∑ p ∈ R.unsatDarts (R.kDecode q T hq A), Cd p i j := by + rw [Finset.sum_comm] + exact Finset.sum_congr rfl fun i _ => Finset.sum_comm + rw [hreorder] + refine le_trans (Finset.sum_le_sum fun i hi => Finset.sum_le_sum fun j hj => ?_) + (R.sum_len_pos_le q T hq A hHT) + have hij : i + j + 1 < T := hsq i hi j hj + have hiT : i ≤ T := by + simp only [Finset.mem_range] at hi + omega + have hjT : (i + j + 1) - (i + 1) ≤ T := by omega + calc ∑ p ∈ R.unsatDarts (R.kDecode q T hq A), Cd p i j + = ∑ p ∈ R.unsatDarts (R.kDecode q T hq A), + (R.crossingSet q T hq A p.1 p.2 hij hiT hjT).card := + Finset.sum_congr rfl fun p hp => hCd p hp i hi j hj hij hiT hjT + _ ≤ (Finset.univ.filter fun z : R.graph.V × R.KLabels q T => + stopAt z.2.2 = i + j + 1 ∧ i ∈ R.goodPos q T hq A z).card := + R.sum_crossingSet_le q T hq A hij hiT hjT + +/-! ### Powering soundness -/ + +/-- **The second-moment bound on unsatisfied constraints.** Given a lower bound +on the first moment of the crossing count and an upper bound on its second +moment, at least `Alb ^ 2 / Bub` of the powered constraints are unsatisfied. -/ +theorem card_unsatDarts_ge (A : (R.killedPow q T hq).Assignment) {Alb Bub : ℝ} + (hA0 : 0 ≤ Alb) + (hA : Alb ≤ ∑ z : R.graph.V × R.KLabels q T, ((R.goodCrossings q T hq A z).card : ℝ)) + (hB : ∑ z : R.graph.V × R.KLabels q T, ((R.goodCrossings q T hq A z).card : ℝ) ^ 2 + ≤ Bub) (hB0 : 0 < Bub) : + Alb ^ 2 / Bub ≤ (((R.killedPow q T hq).unsatDarts A).card : ℝ) := by + classical + refine card_ge_of_moments (fun z => ((R.goodCrossings q T hq A z).card : ℝ)) + ((R.killedPow q T hq).unsatDarts A) ?_ hA0 hA hB hB0 + intro z hz + have hcard : (R.goodCrossings q T hq A z).card ≠ 0 := by + intro h + apply hz + show ((R.goodCrossings q T hq A z).card : ℝ) = 0 + rw [h] + norm_num + exact R.mem_unsatDarts_of_goodCrossings_nonempty q T hq A z + (Finset.card_pos.mp (Nat.pos_of_ne_zero hcard)) + +/-- The crossing count as a total function of the two lengths, so that it can be +summed without carrying the side conditions. -/ +noncomputable def crossCount (A : (R.killedPow q T hq).Assignment) (p : R.Dart) + (i j : ℕ) : ℕ := + if h : i + j + 1 < T then + (R.crossingSet q T hq A p.1 p.2 h (by omega) (by omega)).card + else 0 + +theorem crossCount_eq_card (A : (R.killedPow q T hq).Assignment) (p : R.Dart) + {i j : ℕ} (hij : i + j + 1 < T) (hiT : i ≤ T) (hjT : (i + j + 1) - (i + 1) ≤ T) : + R.crossCount q T hq A p i j = (R.crossingSet q T hq A p.1 p.2 hij hiT hjT).card := by + rw [crossCount, dif_pos hij] + +theorem crossCount_eq_prod (A : (R.killedPow q T hq).Assignment) (p : R.Dart) + {i j : ℕ} (hij : i + j + 1 < T) : + R.crossCount q T hq A p i j + = R.truthCount q T hq A p.1 i * R.truthCount q T hq A (R.graph.nbr p.1 p.2) j + * (R.graph.deg ^ (T - (i + j + 1)) + * ((q - 1) ^ (i + j + 1) * q ^ (T - (i + j + 1) - 1))) := by + rw [R.crossCount_eq_card q T hq A p hij (by omega) (by omega), + R.card_crossingSet q T hq A p.1 p.2 hij (by omega) (by omega)] + +/-- **The first moment, in closed form.** Combining the per-dart bound with the +fact that the counted crossings are good crossings. -/ +theorem sum_goodCrossings_ge (A : (R.killedPow q T hq).Assignment) {H : ℕ} + (hH : 2 * H + 1 < T) (hHT : H + 1 ≤ T) + (hsq : ∀ i ∈ Finset.range (H + 1), ∀ j ∈ Finset.range (H + 1), i + j + 1 < T) : + (((R.unsatDarts (R.kDecode q T hq A)).card + * ((q - 1) * ((R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H) + * (R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H))) : ℕ) : ℝ) + ≤ ((Fintype.card α ^ 2 * (R.graph.deg ^ (T + 1) * q ^ T) : ℕ) : ℝ) + * ∑ z : R.graph.V × R.KLabels q T, ((R.goodCrossings q T hq A z).card : ℝ) := by + classical + have h1 := R.sum_dart_lower q T hq A (R.unsatDarts (R.kDecode q T hq A)) hH hHT + (R.crossCount q T hq A) + (fun p _ i hi j hj => R.crossCount_eq_prod q T hq A p (hsq i hi j hj)) + have h2 := R.sum_Cd_le_sum_goodCrossings q T hq A hHT (R.crossCount q T hq A) + (fun p _ i _ j _ hij hiT hjT => R.crossCount_eq_card q T hq A p hij hiT hjT) hsq + have h3 : (R.unsatDarts (R.kDecode q T hq A)).card + * ((q - 1) * ((R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H) + * (R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H))) + ≤ Fintype.card α ^ 2 * (R.graph.deg ^ (T + 1) * q ^ T) + * ∑ z : R.graph.V × R.KLabels q T, (R.goodCrossings q T hq A z).card := by + refine le_trans h1 ?_ + have hmul : (∑ p ∈ R.unsatDarts (R.kDecode q T hq A), ∑ i ∈ Finset.range (H + 1), + ∑ j ∈ Finset.range (H + 1), R.crossCount q T hq A p i j) + * (R.graph.deg ^ (T + 1) * q ^ T) + ≤ (∑ z : R.graph.V × R.KLabels q T, (R.goodCrossings q T hq A z).card) + * (R.graph.deg ^ (T + 1) * q ^ T) := + Nat.mul_le_mul_right _ h2 + calc Fintype.card α ^ 2 + * ((∑ p ∈ R.unsatDarts (R.kDecode q T hq A), ∑ i ∈ Finset.range (H + 1), + ∑ j ∈ Finset.range (H + 1), R.crossCount q T hq A p i j) + * (R.graph.deg ^ (T + 1) * q ^ T)) + ≤ Fintype.card α ^ 2 + * ((∑ z : R.graph.V × R.KLabels q T, (R.goodCrossings q T hq A z).card) + * (R.graph.deg ^ (T + 1) * q ^ T)) := Nat.mul_le_mul_left _ hmul + _ = Fintype.card α ^ 2 * (R.graph.deg ^ (T + 1) * q ^ T) + * ∑ z : R.graph.V × R.KLabels q T, (R.goodCrossings q T hq A z).card := by ring + have h4 : (((R.unsatDarts (R.kDecode q T hq A)).card + * ((q - 1) * ((R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H) + * (R.graph.deg ^ T * q ^ T + - Fintype.card α * pluralityLoss R.graph.deg q T H))) : ℕ) : ℝ) + ≤ ((Fintype.card α ^ 2 * (R.graph.deg ^ (T + 1) * q ^ T) + * ∑ z : R.graph.V × R.KLabels q T, (R.goodCrossings q T hq A z).card : ℕ) : ℝ) := by + exact_mod_cast h3 + refine le_trans h4 ?_ + push_cast + exact le_refl _ + +omit [Fintype α] [DecidableEq α] [Nonempty α] in +/-- The powered system's constraint count. -/ +theorem card_dart_killedPow : + ((R.killedPow q T hq).graph.order * (R.killedPow q T hq).graph.deg : ℕ) + = R.graph.order * (R.graph.deg ^ T * q ^ T) := by + show ((R.graph.killedPower q T hq).order * (R.graph.killedPower q T hq).deg : ℕ) = _ + rw [R.graph.order_killedPower, R.graph.deg_killedPower] + +omit [DecidableEq α] in +/-- A bound holding for every assignment holds for the value. -/ +theorem le_unsatVal_killedPow {LB : ℝ} + (h : ∀ A : (R.killedPow q T hq).Assignment, + LB ≤ (((R.killedPow q T hq).unsatFrac A : ℚ) : ℝ)) : + LB ≤ (((R.killedPow q T hq).unsatVal : ℚ) : ℝ) := by + obtain ⟨A, hA⟩ := (R.killedPow q T hq).exists_assignment_unsatFrac_eq_unsatVal + rw [← hA] + exact h A + +end RegCSP + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/KilledPlurality.lean b/Complexitylib/Classes/PCP/Internal/KilledPlurality.lean new file mode 100644 index 00000000..d7f3eadf --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/KilledPlurality.lean @@ -0,0 +1,290 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.KilledCSP +public import Complexitylib.Classes.PCP.Internal.FinsetPlurality + +/-! +# Decoding a killed-power assignment + +An assignment of the killed power labels each vertex with opinions that need not +be consistent with one another. Soundness decodes it into a single assignment of +the original system, by plurality, and counts the failed walk constraints against +that. + +The opinion about `v` that a killed walk out of `v` reports is the one its far +end holds, read at the reversed walk — the walk that leads back from that end to +`v`. Letting the killed walk range over all `deg ^ T * q ^ T` labels gives a +multiset of opinions about `v`, and `kPlurality` picks a most frequent one. + +This is the distribution the soundness argument needs, and the reason for the +killed walk law: conditioned on a walk crossing a given dart, its prefix and its +suffix are *independent* killed walks out of that dart's two ends, each governed +by exactly this law. So the plurality bound applies to both ends at once, and +the two bounds multiply. + +## Main definitions + +- `RegCSP.kOpinionAbout` — what the far end of a killed walk says about its start +- `RegCSP.kOpinionCount`, `RegCSP.kPlurality` — the decoded assignment +- `RegCSP.opinionOf` — the report as a function of the effective walk alone +- `RegCSP.card_fibre_opinion` — the reports of one effective length, counted as + walks times the fibre weight +- `RegCSP.kOpinionCount_eq_sum` — the reports split by effective length +- `RegCSP.truthCount`, `RegCSP.card_le_mul_sum_truthCount` — plurality restated + as a weighted sum over the walks out of a vertex + +## Main results + +- `RegCSP.card_le_card_mul_kOpinionCount` — the plurality is reported by at + least a `1 / |α|` fraction of the killed walks out of a vertex +-/ + +@[expose] public section + +namespace Complexity + +namespace RegCSP + +variable {α : Type} [Fintype α] [DecidableEq α] [Nonempty α] +variable (R : RegCSP α) (q T : ℕ) (hq : 0 < q) + +/-- The label sequences of the killed power. -/ +abbrev KLabels (R : RegCSP α) (q T : ℕ) : Type := + (Fin T → R.graph.D) × (Fin T → Fin q) + +/-- What the far end of the killed walk `x` out of `v` says about `v`: its +opinion read at the reversed walk, which leads from that end back to `v`. -/ +def kOpinionAbout (A : (R.killedPow q T hq).Assignment) (v : R.graph.V) + (x : R.KLabels q T) : α := + A (R.graph.killedEnd v x.1 x.2) + ⟨⟨R.graph.kLen x, by have := R.graph.kLen_le x; omega⟩, + R.graph.revWalk v (R.graph.kWalk x)⟩ + +/-- The opinion reported by a killed walk, as a function of its effective length +and effective walk alone. Everything the report depends on — where the walk ends +and the reversed walk it is read at — is determined by those two, so the labels +past the stopping index are free. That is what makes each effective walk carry +the weight `card_killed_fibre` computes. -/ +def opinionOf (A : (R.killedPow q T hq).Assignment) (v : R.graph.V) {m : ℕ} (hm : m ≤ T) + (w : Fin m → R.graph.D) : α := + A (R.graph.walkEnd m v w) ⟨⟨m, by omega⟩, R.graph.revWalk v w⟩ + +omit [Fintype α] [DecidableEq α] [Nonempty α] in +theorem kOpinionAbout_eq_opinionOf (A : (R.killedPow q T hq).Assignment) (v : R.graph.V) + (x : R.KLabels q T) : + R.kOpinionAbout q T hq A v x + = R.opinionOf q T hq A v (stopAt_le x.2) (R.graph.preWalk x.1 (stopAt_le x.2)) := rfl + +omit [Fintype α] [DecidableEq α] [Nonempty α] in +/-- Two effective walks of the same length with the same labels give the same +report. Stated with the length as a hypothesis rather than by rewriting, since +the walk's type mentions it: `subst` does the transport that `rw` cannot. -/ +theorem opinionOf_congr (A : (R.killedPow q T hq).Assignment) (v : R.graph.V) + {m m' : ℕ} (hm : m ≤ T) (hm' : m' ≤ T) (hmm : m = m') + (w : Fin m → R.graph.D) (w' : Fin m' → R.graph.D) + (hw : ∀ (j : ℕ) (hj : j < m) (hj' : j < m'), w ⟨j, hj⟩ = w' ⟨j, hj'⟩) : + R.opinionOf q T hq A v hm w = R.opinionOf q T hq A v hm' w' := by + subst hmm + have hww : w = w' := by + funext j + exact hw j.val j.isLt j.isLt + subst hww + rfl + +omit [Fintype α] [DecidableEq α] [Nonempty α] in +/-- On the fibre of effective length `m`, the report is the one determined by +the length-`m` prefix. -/ +theorem kOpinionAbout_eq_of_stopAt (A : (R.killedPow q T hq).Assignment) (v : R.graph.V) + (x : R.KLabels q T) {m : ℕ} (hm : m ≤ T) (h : stopAt x.2 = m) : + R.kOpinionAbout q T hq A v x + = R.opinionOf q T hq A v hm (R.graph.preWalk x.1 hm) := by + rw [kOpinionAbout_eq_opinionOf] + exact R.opinionOf_congr q T hq A v (stopAt_le x.2) hm h _ _ fun j hj hj' => rfl + +/-- How many killed walks out of `v` ascribe the value `a` to it. -/ +noncomputable def kOpinionCount (A : (R.killedPow q T hq).Assignment) (v : R.graph.V) + (a : α) : ℕ := + (Finset.univ.filter fun x : R.KLabels q T => R.kOpinionAbout q T hq A v x = a).card + +omit [Fintype α] [Nonempty α] in +/-- **The fibre partition.** Among the killed walks of effective length `m`, +those reporting `val` are exactly the ones whose effective walk reports `val`, +each carrying the same weight. So the count splits into a count of *walks* times +that weight. -/ +theorem card_fibre_opinion (A : (R.killedPow q T hq).Assignment) (v : R.graph.V) + (val : α) {m : ℕ} (hm : m < T) : + (Finset.univ.filter fun x : R.KLabels q T => + stopAt x.2 = m ∧ R.kOpinionAbout q T hq A v x = val).card + = (Finset.univ.filter fun w : Fin m → R.graph.D => + R.opinionOf q T hq A v (le_of_lt hm) w = val).card + * (R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1))) := by + classical + have hmaps : ∀ x ∈ (Finset.univ.filter fun x : R.KLabels q T => + stopAt x.2 = m ∧ R.kOpinionAbout q T hq A v x = val), + R.graph.preWalk x.1 (le_of_lt hm) ∈ (Finset.univ.filter + fun w : Fin m → R.graph.D => R.opinionOf q T hq A v (le_of_lt hm) w = val) := by + intro x hx + simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hx ⊢ + rw [← R.kOpinionAbout_eq_of_stopAt q T hq A v x (le_of_lt hm) hx.1] + exact hx.2 + rw [Finset.card_eq_sum_card_fiberwise hmaps] + have hfib : ∀ w ∈ (Finset.univ.filter fun w : Fin m → R.graph.D => + R.opinionOf q T hq A v (le_of_lt hm) w = val), + ((Finset.univ.filter fun x : R.KLabels q T => + stopAt x.2 = m ∧ R.kOpinionAbout q T hq A v x = val).filter + fun x => R.graph.preWalk x.1 (le_of_lt hm) = w).card + = R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1)) := by + intro w hw + simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hw + have hset : ((Finset.univ.filter fun x : R.KLabels q T => + stopAt x.2 = m ∧ R.kOpinionAbout q T hq A v x = val).filter + fun x => R.graph.preWalk x.1 (le_of_lt hm) = w) + = Finset.univ.filter fun x : R.KLabels q T => + R.graph.preWalk x.1 (le_of_lt hm) = w ∧ stopAt x.2 = m := by + ext x + simp only [Finset.mem_filter, Finset.mem_univ, true_and] + constructor + · rintro ⟨⟨hlen, -⟩, hpre⟩ + exact ⟨hpre, hlen⟩ + · rintro ⟨hpre, hlen⟩ + refine ⟨⟨hlen, ?_⟩, hpre⟩ + rw [R.kOpinionAbout_eq_of_stopAt q T hq A v x (le_of_lt hm) hlen, hpre] + exact hw + rw [hset, R.graph.card_killed_fibre hq hm w] + rw [Finset.sum_congr rfl hfib, Finset.sum_const, smul_eq_mul] + +/-- The plurality decoding: every vertex is given a value that the ends of the +killed walks out of it ascribe to it most often. -/ +noncomputable def kPlurality (A : (R.killedPow q T hq).Assignment) (v : R.graph.V) : α := + Classical.choose (exists_plurality (Finset.univ : Finset (R.KLabels q T)) + (R.kOpinionAbout q T hq A v)) + +/-- **Pigeonhole.** At least a `1 / |α|` fraction of the killed walks out of `v` +report the decoded value. -/ +theorem card_le_card_mul_kOpinionCount (A : (R.killedPow q T hq).Assignment) + (v : R.graph.V) : + Fintype.card (R.KLabels q T) + ≤ Fintype.card α * R.kOpinionCount q T hq A v (R.kPlurality q T hq A v) := by + have h := Classical.choose_spec (exists_plurality (Finset.univ : Finset (R.KLabels q T)) + (R.kOpinionAbout q T hq A v)) + exact h + +omit [Fintype α] [Nonempty α] in +/-- The reports split by effective length. -/ +theorem kOpinionCount_eq_sum (A : (R.killedPow q T hq).Assignment) (v : R.graph.V) + (val : α) : + R.kOpinionCount q T hq A v val + = ∑ m ∈ Finset.range (T + 1), + (Finset.univ.filter fun x : R.KLabels q T => + stopAt x.2 = m ∧ R.kOpinionAbout q T hq A v x = val).card := by + classical + have hmaps : ∀ x ∈ (Finset.univ.filter fun x : R.KLabels q T => + R.kOpinionAbout q T hq A v x = val), stopAt x.2 ∈ Finset.range (T + 1) := by + intro x _ + simp only [Finset.mem_range] + have := stopAt_le x.2 + omega + rw [kOpinionCount, Finset.card_eq_sum_card_fiberwise hmaps] + refine Finset.sum_congr rfl fun m _ => ?_ + congr 1 + ext x + simp only [Finset.mem_filter, Finset.mem_univ, true_and] + tauto + +/-- How many length-`m` walks out of `v` have a far end reporting the decoded +value. Total in `m`, so that sums over lengths need no side conditions. -/ +noncomputable def truthCount (A : (R.killedPow q T hq).Assignment) (v : R.graph.V) + (m : ℕ) : ℕ := + if hm : m ≤ T then + (Finset.univ.filter fun w : Fin m → R.graph.D => + R.opinionOf q T hq A v hm w = R.kPlurality q T hq A v).card + else 0 + +theorem card_fibre_truth (A : (R.killedPow q T hq).Assignment) (v : R.graph.V) + {m : ℕ} (hm : m < T) : + (Finset.univ.filter fun x : R.KLabels q T => + stopAt x.2 = m ∧ R.kOpinionAbout q T hq A v x = R.kPlurality q T hq A v).card + = R.truthCount q T hq A v m + * (R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1))) := by + rw [R.card_fibre_opinion q T hq A v _ hm, truthCount, dif_pos (le_of_lt hm)] + +omit [Fintype α] [DecidableEq α] [Nonempty α] in +theorem card_KLabels : Fintype.card (R.KLabels q T) = R.graph.deg ^ T * q ^ T := by + rw [Fintype.card_prod, Fintype.card_fun, Fintype.card_fun, Fintype.card_fin, + Fintype.card_fin] + rfl + +omit [Fintype α] [DecidableEq α] [Nonempty α] in +/-- The killed walks that never stop. This is the truncation term, and it is an +exponentially small fraction `((q-1)/q) ^ T` of all labels. -/ +theorem card_stopAt_eq_top : + (Finset.univ.filter fun x : R.KLabels q T => stopAt x.2 = T).card + = R.graph.deg ^ T * (q - 1) ^ T := by + classical + have hsig : (Finset.univ.filter fun c : Fin T → Fin q => stopAt c = T).card + = (q - 1) ^ T := by + have hsame : (Finset.univ.filter fun c : Fin T → Fin q => stopAt c = T) + = (Finset.univ.filter fun c : Fin T → Fin q => T ≤ stopAt c) := by + ext c + have := stopAt_le c + simp only [Finset.mem_filter, Finset.mem_univ, true_and] + omega + rw [hsame, card_le_stopAt (le_refl T)] + simp + have hset : (Finset.univ.filter fun x : R.KLabels q T => stopAt x.2 = T) + = (Finset.univ : Finset (Fin T → R.graph.D)) + ×ˢ (Finset.univ.filter fun c : Fin T → Fin q => stopAt c = T) := by + ext x + simp only [Finset.mem_filter, Finset.mem_univ, true_and, Finset.mem_product] + rw [hset, Finset.card_product, hsig, Finset.card_univ, Fintype.card_fun, + Fintype.card_fin] + rfl + +/-- **Plurality, in terms of walks.** At least a `1 / |α|` fraction of all +killed-walk labels report the decoded value, and splitting that count by +effective length turns it into a statement about the *walks* out of `v`: the +weighted sum of `truthCount` is large, up to the exponentially small truncation +term of walks that never stop. + +This is the form the first moment consumes, since the crossing decomposition +produces exactly these weighted sums — one for the prefix and one for the +suffix, with independent lengths. -/ +theorem card_le_mul_sum_truthCount (A : (R.killedPow q T hq).Assignment) + (v : R.graph.V) : + R.graph.deg ^ T * q ^ T + ≤ Fintype.card α * ((∑ m ∈ Finset.range T, + R.truthCount q T hq A v m + * (R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1)))) + + R.graph.deg ^ T * (q - 1) ^ T) := by + classical + have h1 := R.card_le_card_mul_kOpinionCount q T hq A v + rw [R.card_KLabels q T, R.kOpinionCount_eq_sum q T hq A v, Finset.sum_range_succ] at h1 + have h2 : ∀ m ∈ Finset.range T, + (Finset.univ.filter fun x : R.KLabels q T => + stopAt x.2 = m ∧ R.kOpinionAbout q T hq A v x = R.kPlurality q T hq A v).card + = R.truthCount q T hq A v m + * (R.graph.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1))) := by + intro m hm + exact R.card_fibre_truth q T hq A v (Finset.mem_range.mp hm) + rw [Finset.sum_congr rfl h2] at h1 + have h3 : (Finset.univ.filter fun x : R.KLabels q T => + stopAt x.2 = T ∧ R.kOpinionAbout q T hq A v x = R.kPlurality q T hq A v).card + ≤ R.graph.deg ^ T * (q - 1) ^ T := by + rw [← R.card_stopAt_eq_top q T] + refine Finset.card_le_card ?_ + intro x hx + simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hx ⊢ + exact hx.1 + exact le_trans h1 (Nat.mul_le_mul_left _ (Nat.add_le_add_left h3 _)) + +/-- The decoded assignment of the original system. -/ +noncomputable def kDecode (A : (R.killedPow q T hq).Assignment) : R.Assignment := + fun v => R.kPlurality q T hq A v + +end RegCSP + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/KilledWalk.lean b/Complexitylib/Classes/PCP/Internal/KilledWalk.lean new file mode 100644 index 00000000..53b31a1b --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/KilledWalk.lean @@ -0,0 +1,388 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.Power + +/-! +# Killed walks + +The walk law Dinur's powering step actually needs: at every step the walk stops +with probability `1 / q`, so its length is geometric rather than fixed. + +A dart label is a pair of sequences: `T` edge labels and `T` stop signals. The +walk follows the edge labels until the first signal equal to `0` — the +**stopping index** — and its endpoint is where it stopped. Uniform labels +therefore realise a geometric length exactly, truncated at `T`, with the labels +past the stopping index carrying multiplicity but no meaning. + +## Why geometric and not fixed length + +Soundness decodes an assignment of the powered system by plurality and needs the +opinions at the two ends of a checked step to be truthful. Summed over the +checked steps that amounts to `∑ a_i · b_j` over the prefix length `i` and the +suffix length `j`. With walks of *fixed* length those are tied by `i + j = t - 1` +and the sum can vanish — the two ends can be truthful at complementary lengths +and never at the same step, so nothing is charged. Killing makes +`i` and `j` **independent**, so the sum factorises into a product of two +plurality bounds. That factorisation is the whole reason for this construction. + +## Reversal + +Reversing a killed walk reverses only its effective prefix and leaves both the +tail and the stop signals alone. Since the signals are untouched, the reversed +walk stops at the same index, which is what makes reversal an involution — the +requirement for the powered graph to be an undirected regular graph at all. + +## Main definitions + +- `stopAt` — the stopping index of a signal sequence +- `RegGraph.preWalk`, `RegGraph.extWalk` — the effective prefix, and + overwriting it +- `RegGraph.killedEnd`, `RegGraph.killedRev` — the endpoint and the reversal +- `RegGraph.killedPower` — the resulting regular graph + +## Main results + +- `lt_stopAt_iff` — reaching step `i` depends only on the first `i + 1` signals +- `card_lt_stopAt`, `card_le_stopAt`, `card_stopAt_eq` — how many signal + sequences run for a given number of steps +- `RegGraph.card_preWalk_eq`, `RegGraph.card_killed_fibre` — the weight an + effective walk carries +- `RegGraph.killedEnd_killedRev`, `RegGraph.killedRev_killedRev` — reversal + undoes itself +- `RegGraph.deg_killedPower` — the degree is `deg ^ T * q ^ T` +-/ + +@[expose] public section + +namespace Complexity + +/-- The step at which a killed walk stops: the first index carrying the signal +`0`, or `T` if there is none. -/ +def stopAt {T q : ℕ} (c : Fin T → Fin q) : ℕ := + (List.finRange T).findIdx fun i => (c i).val == 0 + +theorem stopAt_le {T q : ℕ} (c : Fin T → Fin q) : stopAt c ≤ T := by + have h := List.findIdx_le_length (p := fun i : Fin T => (c i).val == 0) + (xs := List.finRange T) + simpa using h + +/-- Before the stopping index every signal is nonzero. -/ +theorem signal_ne_zero_of_lt {T q : ℕ} (c : Fin T → Fin q) {j : ℕ} (hj : j < stopAt c) : + (c ⟨j, lt_of_lt_of_le hj (stopAt_le c)⟩).val ≠ 0 := by + have h := List.not_of_lt_findIdx (p := fun i : Fin T => (c i).val == 0) + (xs := List.finRange T) hj + rw [List.getElem_finRange] at h + simpa using h + +/-- At the stopping index, if it is reached, the signal is zero. -/ +theorem signal_eq_zero_of_stopAt_lt {T q : ℕ} (c : Fin T → Fin q) (h : stopAt c < T) : + (c ⟨stopAt c, h⟩).val = 0 := by + have hlen : stopAt c < (List.finRange T).length := by simpa using h + have hg := List.findIdx_getElem (p := fun i : Fin T => (c i).val == 0) + (xs := List.finRange T) (w := hlen) + rw [List.getElem_finRange] at hg + simpa using hg + +/-- The walk is still running at step `i` exactly when the first `i + 1` +signals are all nonzero. This is the form the crossing decomposition needs: the +event "the walk reaches step `i`" depends only on the first `i + 1` signal +coordinates, so it splits off as a factor. -/ +theorem lt_stopAt_iff {T q : ℕ} (c : Fin T → Fin q) {i : ℕ} (hi : i < T) : + i < stopAt c ↔ ∀ (j : ℕ) (hj : j ≤ i), (c ⟨j, lt_of_le_of_lt hj hi⟩).val ≠ 0 := by + constructor + · intro h j hj + exact signal_ne_zero_of_lt c (lt_of_le_of_lt hj h) + · intro h + by_contra hcon + have hle : stopAt c ≤ i := by omega + have hsT : stopAt c < T := lt_of_le_of_lt hle hi + exact h (stopAt c) hle (signal_eq_zero_of_stopAt_lt c hsT) + +/-- **The signal count.** The walks still running at step `i` are exactly those +whose first `i + 1` signals are all nonzero, and those coordinates are free of +one another, so they number `(q-1) ^ (i+1) * q ^ (T-i-1)`. This is the factor +that the crossing decomposition peels off. -/ +theorem card_lt_stopAt {T q : ℕ} {i : ℕ} (hi : i < T) : + (Finset.univ.filter fun c : Fin T → Fin q => i < stopAt c).card + = (q - 1) ^ (i + 1) * q ^ (T - i - 1) := by + classical + have hset : (Finset.univ.filter fun c : Fin T → Fin q => i < stopAt c) + = Fintype.piFinset fun j : Fin T => + if j.val ≤ i then Finset.univ.filter (fun x : Fin q => x.val ≠ 0) + else Finset.univ := by + ext c + simp only [Finset.mem_filter, Finset.mem_univ, true_and, Fintype.mem_piFinset] + rw [lt_stopAt_iff c hi] + constructor + · intro h j + by_cases hj : j.val ≤ i + · simp only [hj, if_true, Finset.mem_filter, Finset.mem_univ, true_and] + simpa using h j.val hj + · simp [hj] + · intro h j hj + have hmem := h ⟨j, lt_of_le_of_lt hj hi⟩ + simp only [hj, if_true, Finset.mem_filter, Finset.mem_univ, true_and] at hmem + exact hmem + have hnz : (Finset.univ.filter (fun x : Fin q => x.val ≠ 0)).card = q - 1 := by + rcases Nat.eq_zero_or_pos q with hq0 | hq0 + · subst hq0 + simp + · have hcompl : (Finset.univ.filter (fun x : Fin q => x.val ≠ 0)).card + + (Finset.univ.filter (fun x : Fin q => ¬ x.val ≠ 0)).card = q := by + rw [Finset.card_filter_add_card_filter_not] + simp + have hone : (Finset.univ.filter (fun x : Fin q => ¬ x.val ≠ 0)).card = 1 := by + have hsingle : (Finset.univ.filter (fun x : Fin q => ¬ x.val ≠ 0)) = {⟨0, hq0⟩} := by + ext x + simp only [Finset.mem_filter, Finset.mem_univ, true_and, not_not, + Finset.mem_singleton] + constructor + · intro hx; exact Fin.ext hx + · intro hx; rw [hx] + rw [hsingle, Finset.card_singleton] + omega + have hle : (Finset.univ.filter fun j : Fin T => j.val ≤ i).card = i + 1 := by + have himg : (Finset.univ.filter fun j : Fin T => j.val ≤ i).image Fin.val + = Finset.range (i + 1) := by + ext n + simp only [Finset.mem_image, Finset.mem_filter, Finset.mem_univ, true_and, + Finset.mem_range] + constructor + · rintro ⟨j, hj, rfl⟩; omega + · intro hn + exact ⟨⟨n, by omega⟩, by simpa using by omega, rfl⟩ + have hcard := congrArg Finset.card himg + rw [Finset.card_image_of_injective _ Fin.val_injective, Finset.card_range] at hcard + exact hcard + have hgt : (Finset.univ.filter fun j : Fin T => ¬ j.val ≤ i).card = T - i - 1 := by + have hsum : (Finset.univ.filter fun j : Fin T => j.val ≤ i).card + + (Finset.univ.filter fun j : Fin T => ¬ j.val ≤ i).card = T := by + rw [Finset.card_filter_add_card_filter_not] + simp + omega + rw [hset, Fintype.card_piFinset] + simp only [apply_ite Finset.card, hnz, Finset.card_univ, Fintype.card_fin] + rw [Finset.prod_ite, Finset.prod_const, Finset.prod_const, hle, hgt] + +/-- The walks that are still running *at* step `m`, i.e. run for at least `m` +steps. -/ +theorem card_le_stopAt {T q : ℕ} {m : ℕ} (hm : m ≤ T) : + (Finset.univ.filter fun c : Fin T → Fin q => m ≤ stopAt c).card + = (q - 1) ^ m * q ^ (T - m) := by + cases m with + | zero => + have huniv : (Finset.univ.filter fun c : Fin T → Fin q => 0 ≤ stopAt c) + = Finset.univ := by + ext c + simp + rw [huniv] + simp + | succ k => + have hk : k < T := by omega + have hsame : (Finset.univ.filter fun c : Fin T → Fin q => k + 1 ≤ stopAt c) + = (Finset.univ.filter fun c : Fin T → Fin q => k < stopAt c) := by + ext c + simp + rw [hsame, card_lt_stopAt hk] + congr 1 + +/-- **The exact-length signal count.** The walks of effective length exactly `m` +number `(q-1) ^ m * q ^ (T-m-1)`: the first `m` signals are nonzero, the `m`-th +is zero, and the rest are free. -/ +theorem card_stopAt_eq {T q : ℕ} (hq : 0 < q) {m : ℕ} (hm : m < T) : + (Finset.univ.filter fun c : Fin T → Fin q => stopAt c = m).card + = (q - 1) ^ m * q ^ (T - m - 1) := by + classical + have hsub : (Finset.univ.filter fun c : Fin T → Fin q => m < stopAt c) + ⊆ (Finset.univ.filter fun c : Fin T → Fin q => m ≤ stopAt c) := by + intro c hc + simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hc ⊢ + omega + have hdiff : (Finset.univ.filter fun c : Fin T → Fin q => stopAt c = m) + = (Finset.univ.filter fun c : Fin T → Fin q => m ≤ stopAt c) + \ (Finset.univ.filter fun c : Fin T → Fin q => m < stopAt c) := by + ext c + simp only [Finset.mem_filter, Finset.mem_univ, true_and, Finset.mem_sdiff, not_lt] + omega + rw [hdiff, Finset.card_sdiff, Finset.inter_eq_left.mpr hsub, + card_le_stopAt (le_of_lt hm), card_lt_stopAt hm] + have hsplit : q ^ (T - m) = q * q ^ (T - m - 1) := by + rw [← pow_succ'] + congr 1 + omega + rw [hsplit, pow_succ] + cases q with + | zero => omega + | succ p => + have hone : p + 1 - 1 = p := by omega + rw [hone] + refine Nat.sub_eq_of_eq_add ?_ + ring + +namespace RegGraph + +variable (G : RegGraph) + +/-! ### Prefixes -/ + +/-- The first `ℓ` labels of a length-`T` sequence. -/ +def preWalk {T : ℕ} (s : Fin T → G.D) {ℓ : ℕ} (h : ℓ ≤ T) : Fin ℓ → G.D := + fun j => s ⟨j.val, lt_of_lt_of_le j.isLt h⟩ + +/-- Overwrite the first `ℓ` labels of a sequence. -/ +def extWalk {T ℓ : ℕ} (r : Fin ℓ → G.D) (s : Fin T → G.D) : Fin T → G.D := + fun i => if h : i.val < ℓ then r ⟨i.val, h⟩ else s i + +theorem preWalk_extWalk {T ℓ : ℕ} (h : ℓ ≤ T) (r : Fin ℓ → G.D) (s : Fin T → G.D) : + G.preWalk (G.extWalk r s) h = r := by + funext j + simp [preWalk, extWalk, j.isLt] + +theorem extWalk_extWalk {T ℓ : ℕ} (r r' : Fin ℓ → G.D) (s : Fin T → G.D) : + G.extWalk r' (G.extWalk r s) = G.extWalk r' s := by + funext i + by_cases hi : i.val < ℓ <;> simp [extWalk, hi] + +theorem extWalk_preWalk {T ℓ : ℕ} (h : ℓ ≤ T) (s : Fin T → G.D) : + G.extWalk (G.preWalk s h) s = s := by + funext i + by_cases hi : i.val < ℓ <;> simp [extWalk, preWalk, hi] + +/-- How many label sequences begin with a prescribed length-`m` walk: the first +`m` coordinates are pinned and the rest are free. -/ +theorem card_preWalk_eq {T m : ℕ} (hm : m ≤ T) (w : Fin m → G.D) : + (Finset.univ.filter fun s : Fin T → G.D => G.preWalk s hm = w).card + = G.deg ^ (T - m) := by + classical + have hset : (Finset.univ.filter fun s : Fin T → G.D => G.preWalk s hm = w) + = Fintype.piFinset fun j : Fin T => + if hj : j.val < m then {w ⟨j.val, hj⟩} else Finset.univ := by + ext s + simp only [Finset.mem_filter, Finset.mem_univ, true_and, Fintype.mem_piFinset] + constructor + · intro h j + by_cases hj : j.val < m + · simp only [hj, dif_pos, Finset.mem_singleton] + have hval := congrFun h ⟨j.val, hj⟩ + simpa using hval + · simp [hj] + · intro h + funext j + have hmem := h ⟨j.val, lt_of_lt_of_le j.isLt hm⟩ + simp only [j.isLt, dif_pos, Finset.mem_singleton] at hmem + simpa using hmem + have hlt : (Finset.univ.filter fun j : Fin T => j.val < m).card = m := by + have himg : (Finset.univ.filter fun j : Fin T => j.val < m).image Fin.val + = Finset.range m := by + ext n + simp only [Finset.mem_image, Finset.mem_filter, Finset.mem_univ, true_and, + Finset.mem_range] + constructor + · rintro ⟨j, hj, rfl⟩; exact hj + · intro hn + exact ⟨⟨n, by omega⟩, by simpa using hn, rfl⟩ + have hcard := congrArg Finset.card himg + rw [Finset.card_image_of_injective _ Fin.val_injective, Finset.card_range] at hcard + exact hcard + have hge : (Finset.univ.filter fun j : Fin T => ¬ j.val < m).card = T - m := by + have hsum : (Finset.univ.filter fun j : Fin T => j.val < m).card + + (Finset.univ.filter fun j : Fin T => ¬ j.val < m).card = T := by + rw [Finset.card_filter_add_card_filter_not] + simp + omega + rw [hset, Fintype.card_piFinset] + simp only [apply_dite Finset.card, Finset.card_singleton, Finset.card_univ, + dite_eq_ite] + rw [Finset.prod_ite, Finset.prod_const, Finset.prod_const, hlt, hge] + simp + +/-- **The fibre count.** The killed-walk labels whose effective walk is exactly +a prescribed length-`m` walk number `deg ^ (T-m) * (q-1) ^ m * q ^ (T-m-1)`: the +two conditions constrain the edge labels and the signals separately, so the +counts multiply. This is the weight each effective walk carries, and it is +geometric in `m` — the law both the plurality and the conditional prefix and +suffix follow. -/ +theorem card_killed_fibre {T q m : ℕ} (hq : 0 < q) (hm : m < T) (w : Fin m → G.D) : + (Finset.univ.filter fun x : (Fin T → G.D) × (Fin T → Fin q) => + G.preWalk x.1 (le_of_lt hm) = w ∧ stopAt x.2 = m).card + = G.deg ^ (T - m) * ((q - 1) ^ m * q ^ (T - m - 1)) := by + classical + have hset : (Finset.univ.filter fun x : (Fin T → G.D) × (Fin T → Fin q) => + G.preWalk x.1 (le_of_lt hm) = w ∧ stopAt x.2 = m) + = (Finset.univ.filter fun s : Fin T → G.D => G.preWalk s (le_of_lt hm) = w) + ×ˢ (Finset.univ.filter fun c : Fin T → Fin q => stopAt c = m) := by + ext x + simp only [Finset.mem_filter, Finset.mem_univ, true_and, Finset.mem_product] + rw [hset, Finset.card_product, G.card_preWalk_eq (le_of_lt hm) w, card_stopAt_eq hq hm] + +/-! ### Killed walks -/ + +/-- Where a killed walk ends: it follows the edge labels up to the stopping +index. -/ +def killedEnd {T q : ℕ} (v : G.V) (s : Fin T → G.D) (c : Fin T → Fin q) : G.V := + G.walkEnd (stopAt c) v (G.preWalk s (stopAt_le c)) + +/-- A killed walk reversed: the effective prefix is reversed, the tail and the +stop signals are left alone. -/ +def killedRev {T q : ℕ} (v : G.V) (s : Fin T → G.D) (c : Fin T → Fin q) : Fin T → G.D := + G.extWalk (G.revWalk v (G.preWalk s (stopAt_le c))) s + +theorem killedEnd_killedRev {T q : ℕ} (v : G.V) (s : Fin T → G.D) (c : Fin T → Fin q) : + G.killedEnd (G.killedEnd v s c) (G.killedRev v s c) c = v := by + rw [killedEnd, killedRev, killedEnd, G.preWalk_extWalk (stopAt_le c)] + exact G.walkEnd_revWalk v (G.preWalk s (stopAt_le c)) + +theorem killedRev_killedRev {T q : ℕ} (v : G.V) (s : Fin T → G.D) (c : Fin T → Fin q) : + G.killedRev (G.killedEnd v s c) (G.killedRev v s c) c = s := by + rw [killedRev, killedRev, G.preWalk_extWalk (stopAt_le c), killedEnd, + G.revWalk_revWalk, G.extWalk_extWalk, G.extWalk_preWalk] + +/-! ### The killed power graph -/ + +/-- The killed power of `G`: a dart label is `T` edge labels together with `T` +stop signals, and the neighbour is where the walk stops. -/ +def killedPower (G : RegGraph) (q T : ℕ) (hq : 0 < q) : RegGraph where + V := G.V + D := (Fin T → G.D) × (Fin T → Fin q) + decEqV := G.decEqV + decEqD := by + haveI := G.decEqD + infer_instance + fintypeV := G.fintypeV + fintypeD := by + haveI := G.fintypeD + haveI := G.decEqD + infer_instance + nonemptyD := by + haveI := G.nonemptyD + haveI : Nonempty (Fin q) := ⟨⟨0, hq⟩⟩ + infer_instance + rot x := (G.killedEnd x.1 x.2.1 x.2.2, (G.killedRev x.1 x.2.1 x.2.2, x.2.2)) + rot_involutive := by + rintro ⟨v, s, c⟩ + dsimp only + rw [G.killedEnd_killedRev, G.killedRev_killedRev] + +@[simp] theorem V_killedPower (q T : ℕ) (hq : 0 < q) : (G.killedPower q T hq).V = G.V := rfl + +@[simp] theorem order_killedPower (q T : ℕ) (hq : 0 < q) : + (G.killedPower q T hq).order = G.order := rfl + +/-- The killed power is regular of degree `deg ^ T * q ^ T`. -/ +theorem deg_killedPower (q T : ℕ) (hq : 0 < q) : + (G.killedPower q T hq).deg = G.deg ^ T * q ^ T := by + have h : Fintype.card ((Fin T → G.D) × (Fin T → Fin q)) = G.deg ^ T * q ^ T := by + rw [Fintype.card_prod, Fintype.card_fun, Fintype.card_fun, Fintype.card_fin, + Fintype.card_fin] + rfl + calc (G.killedPower q T hq).deg + = Fintype.card ((Fin T → G.D) × (Fin T → Fin q)) := Fintype.card_congr (Equiv.refl _) + _ = G.deg ^ T * q ^ T := h + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/ListEncode.lean b/Complexitylib/Classes/PCP/Internal/ListEncode.lean new file mode 100644 index 00000000..e728c74c --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/ListEncode.lean @@ -0,0 +1,169 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.PolyRuler +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.PCP.Internal.NatEncode + +/-! +# Writing out a list of encoded entries + +The encoding of a list is its entries' encodings run together inside one pair of +brackets. So a machine that can produce each entry's encoding can produce the +list's, by accumulating them in a loop. + +The loop is `iterate_mem_FP`, and the state carries the accumulated bits, the +counter, and the input the entries are read from. + +## Main definitions + +- `Complexity.listStep` — append the next entry's encoding + +## Main results + +- `Complexity.listStep_iterate` — what the loop accumulates +- `Complexity.bitstringEncode_of_entries` — the accumulation is the encoding +- `Complexity.listEncFn_mem_FP`, `Complexity.listEncFn_eq` — the loop is + polynomial time and writes the encoding +-/ + +@[expose] public section + +namespace Complexity + +/-- One step: append the next entry's encoding and advance the counter. The +state is `pair (pair accumulated counter) input`. -/ +def listStep (E : List Bool → List Bool) (st : List Bool) : List Bool := + pair (pair (pairFst (pairFst st) + ++ E (pair (pairSnd st) (pairSnd (pairFst st)))) + (true :: pairSnd (pairFst st))) (pairSnd st) + +theorem listStep_mem_FP {E : List Bool → List Bool} (hE : E ∈ FP) : listStep E ∈ FP := by + have hacc : (fun st : List Bool => pairFst (pairFst st)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + have hctr : (fun st : List Bool => pairSnd (pairFst st)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + have hx : (fun st : List Bool => pairSnd st) ∈ FP := Cobham.sndBlock_mem_FP + have hE' : (fun st : List Bool => + E (pair (pairSnd st) (pairSnd (pairFst st)))) ∈ FP := by + have := mem_FP_comp (Cobham.pairFn_mem_FP hx hctr) hE + simpa using this + exact Cobham.pairFn_mem_FP + (Cobham.pairFn_mem_FP (Cobham.appendFn_mem_FP hacc hE') + (mem_FP_comp hctr (Cobham.cons_mem_FP true))) hx + +/-- The bits the loop has accumulated after `n` steps. -/ +def entryCat (E : List Bool → List Bool) (x : List Bool) (n : ℕ) : List Bool := + (List.range n).flatMap fun i => E (pair x (List.replicate i true)) + +@[simp] theorem entryCat_zero (E : List Bool → List Bool) (x : List Bool) : + entryCat E x 0 = [] := by + rw [entryCat] + simp + +theorem entryCat_succ (E : List Bool → List Bool) (x : List Bool) (n : ℕ) : + entryCat E x (n + 1) = entryCat E x n ++ E (pair x (List.replicate n true)) := by + rw [entryCat, entryCat, List.range_succ, List.flatMap_append] + simp + +/-- **What the loop accumulates.** -/ +theorem listStep_iterate (E : List Bool → List Bool) (x : List Bool) : + ∀ n : ℕ, (listStep E)^[n] (pair (pair [] []) x) + = pair (pair (entryCat E x n) (List.replicate n true)) x := by + intro n + induction n with + | zero => simp + | succ n ih => + rw [Function.iterate_succ_apply', ih, listStep, pairFst_pair, + pairSnd_pair, pairFst_pair, pairSnd_pair, + entryCat_succ, List.replicate_succ] + +/-- **The accumulation is the encoding.** If each step writes the encoding of +the corresponding entry, the loop writes the inner part of the list's own +encoding. -/ +theorem bitstringEncode_of_entries {α : Type} [DataEncode α] + {E : List Bool → List Bool} {x : List Bool} (l : List α) + (h : ∀ i, ∀ hi : i < l.length, + E (pair x (List.replicate i true)) = DataEncode.bitstringEncode (l[i]'hi)) : + DataEncode.bitstringEncode l = false :: entryCat E x l.length ++ [true] := by + have hcat : entryCat E x l.length = (l.map DataEncode.bitstringEncode).flatten := by + rw [entryCat] + have : (List.range l.length).map (fun i => E (pair x (List.replicate i true))) + = l.map DataEncode.bitstringEncode := by + refine List.ext_getElem (by simp) fun i h1 h2 => ?_ + have hi : i < l.length := by simpa using h2 + rw [List.getElem_map, List.getElem_map, List.getElem_range] + exact h i hi + rw [List.flatMap_def, this] + rw [hcat, DataEncode.bitstringEncode_def, + show DataEncode.encode l = Data.l (l.map DataEncode.encode) from rfl, + Data.toBits_l, List.map_map] + congr 2 + +theorem length_entryCat_le (E : List Bool → List Bool) (x : List Bool) (b : ℕ) : + ∀ n, (∀ i < n, (E (pair x (List.replicate i true))).length ≤ b) → + (entryCat E x n).length ≤ n * b := by + intro n + induction n with + | zero => intro _; simp + | succ n ih => + intro h + have hih := ih fun i hi => h i (by omega) + rw [entryCat_succ, List.length_append] + have hn := h n (by omega) + have hexp : (n + 1) * b = n * b + b := by ring + omega + +/-! ### The loop as one function -/ + +/-- **The list encoder**, on `pair (unary count) input`. -/ +noncomputable def listEncFn (E : List Bool → List Bool) (z : List Bool) : List Bool := + false :: pairFst (pairFst + ((listStep E)^[(pairFst z).length] (pair (pair [] []) (pairSnd z)))) + ++ [true] + +theorem listEncFn_mem_FP {E : List Bool → List Bool} (hE : E ∈ FP) (p : Polynomial ℕ) + (hbound : ∀ z : List Bool, ∀ k ≤ (pairFst z).length, + ((listStep E)^[k] (pair (pair [] []) (pairSnd z))).length + ≤ p.eval z.length) : + listEncFn E ∈ FP := by + have hinit : (fun z : List Bool => pair (pair [] []) (pairSnd z)) ∈ FP := + Cobham.pairFn_mem_FP (constFn_mem_FP (pair [] [])) Cobham.sndBlock_mem_FP + have hwidth : (fun z : List Bool => polyRuler p (id z)) ∈ FP := + polyRulerFn_mem_FP p id_mem_FP + have hbound' : ∀ z : List Bool, ∀ k ≤ (pairFst z).length, + ((listStep E)^[k] (pair (pair [] []) (pairSnd z))).length + ≤ (polyRuler p (id z)).length := by + intro z k hk + rw [polyRuler_length] + exact hbound z k hk + have hiter := Cobham.iterate_mem_FP (listStep_mem_FP hE) hinit + Cobham.fstBlock_mem_FP hwidth hbound' + have hproj := mem_FP_comp (mem_FP_comp hiter Cobham.fstBlock_mem_FP) + Cobham.fstBlock_mem_FP + have hcons := mem_FP_comp hproj (Cobham.cons_mem_FP false) + have := Cobham.appendFn_mem_FP hcons (constFn_mem_FP [true]) + refine mem_FP_of_eq this fun z => ?_ + rw [listEncFn] + simp + +theorem listEncFn_eq (E : List Bool → List Bool) (z : List Bool) : + listEncFn E z + = false :: entryCat E (pairSnd z) (pairFst z).length ++ [true] := by + rw [listEncFn, listStep_iterate, pairFst_pair, pairFst_pair] + +/-- **The loop writes the list's encoding.** -/ +theorem listEncFn_eq_bitstringEncode {α : Type} [DataEncode α] + {E : List Bool → List Bool} {z : List Bool} (l : List α) + (hn : (pairFst z).length = l.length) + (h : ∀ i, ∀ hi : i < l.length, + E (pair (pairSnd z) (List.replicate i true)) + = DataEncode.bitstringEncode (l[i]'hi)) : + listEncFn E z = DataEncode.bitstringEncode l := by + rw [listEncFn_eq, hn, ← bitstringEncode_of_entries l h] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/LocalTest.lean b/Complexitylib/Classes/PCP/Internal/LocalTest.lean new file mode 100644 index 00000000..bc721642 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/LocalTest.lean @@ -0,0 +1,298 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.ConstraintGraph +public import Complexitylib.Classes.PCP.Internal.CubeBlocks +public import Complexitylib.Classes.PCP.Internal.NumEnc + +/-! +# From a many-query test to a binary constraint graph + +An assignment tester reads a handful of positions of its proof per random +string, but a constraint graph checks *two* vertices per edge. The standard +repair is to add a vertex for each random string, labelled by the answers the +tester expects to see, and to connect it to each position it reads: the edge +checks that the claimed answer matches the position's actual bit and that the +claimed answers together pass the test. A rejecting random string then has at +least one of its `q` edges violated — either the claimed answers fail the test +outright, or they pass and so must disagree with the proof somewhere. + +This module carries out that transformation for a **family** of tests indexed by +a type `E` — one per edge of the outer graph, in the composition — over a shared +set of positions. The edges of the result are the triples `(e, z, i)`: test `e`, +random string `z`, read `i`. + +## Main definitions + +- `Complexity.MultiTest` — a family of many-query tests on a shared proof +- `Complexity.MultiTest.toGraph` — the binary constraint graph + +## Main results + +- `Complexity.MultiTest.card_rejects_le_card_unsatEdges` — every rejecting + random string accounts for a distinct violated edge +- `Complexity.MultiTest.satisfiable_toGraph` — a proof accepted everywhere + gives a satisfying assignment +-/ + +@[expose] public section + +namespace Complexity + +open BooleanAnalysis + +/-- A family of tests, indexed by `E`, reading a shared proof over positions +`Pos`. A test uses `R` random bits, reads `q` positions chosen from its random +string, and decides from the bits it read. -/ +structure MultiTest (Pos E Q : Type) where + /-- The number of random bits. -/ + R : ℕ + /-- The positions read, indexed by `Q`, as a function of the test and its + random string. -/ + pos : E → Cube R → Q → Pos + /-- The verdict, as a function of the bits read. -/ + check : E → Cube R → (Q → ZMod 2) → Bool + +namespace MultiTest + +variable {Pos E Q : Type} (M : MultiTest Pos E Q) + +/-- A proof: one bit per position. -/ +abbrev Table (Pos : Type) : Type := Pos → ZMod 2 + +/-- Test `e` accepts the proof `T` on random string `z`. -/ +def accepts (T : Table Pos) (e : E) (z : Cube M.R) : Bool := + M.check e z fun i => T (M.pos e z i) + +/-- The random strings on which test `e` rejects `T`. -/ +def rejects (T : Table Pos) (e : E) : Finset (Cube M.R) := + Finset.univ.filter fun z => M.accepts T e z = false + +/-! ### The binary graph -/ + +/-- The vertices of the binary graph: the positions, and one vertex per +(test, random string). -/ +abbrev Vert : Type := Pos ⊕ (E × Cube M.R) + +/-- The edges: one per (test, random string, read). -/ +abbrev Edge : Type := E × Cube M.R × Q + +/-- The alphabet: a bit (used at positions) paired with a tuple of claimed +answers (used at test vertices). -/ +abbrev Alpha (Q : Type) : Type := ZMod 2 × (Q → ZMod 2) + +/-- A random string is numbered by its own enumeration: the number of random +bits is a constant, so this is a lookup on a bounded key. -/ +noncomputable instance (priority := 2000) instNumEncCube (n : ℕ) : NumEnc (Cube n) := + NumEnc.ofFintype _ + +section Graph + +variable [Fintype Pos] [Fintype E] [Fintype Q] [NumEnc Pos] [NumEnc E] [NumEnc Q] + +/-- The edge a `Fin` index stands for. -/ +noncomputable def edgeOf (k : Fin (Fintype.card M.Edge)) : M.Edge := + (NumEnc.equivFinCard M.Edge).symm k + +/-- The `Fin` index of a vertex. -/ +noncomputable def vertIdx (v : M.Vert) : Fin (Fintype.card M.Vert) := + NumEnc.equivFinCard M.Vert v + +/-- **The binary constraint graph.** Edge `(e, z, i)` runs from the test vertex +`(e, z)` to the position `pos e z i`, and holds when the test vertex's claimed +answers pass test `e` on `z` and its `i`-th claimed answer is the position's +bit. -/ +noncomputable def toGraph : ConstraintGraph (Alpha Q) where + numVerts := Fintype.card M.Vert + numEdges := Fintype.card M.Edge + tail := fun k => M.vertIdx (Sum.inr ((M.edgeOf k).1, (M.edgeOf k).2.1)) + head := fun k => M.vertIdx (Sum.inl (M.pos (M.edgeOf k).1 (M.edgeOf k).2.1 (M.edgeOf k).2.2)) + rel := fun k l₁ l₂ => + decide (M.check (M.edgeOf k).1 (M.edgeOf k).2.1 l₁.2 = true + ∧ l₁.2 (M.edgeOf k).2.2 = l₂.1) + +/-- The proof an assignment of the binary graph carries at its positions. -/ +noncomputable def tableOf (A : M.toGraph.Assignment) : Table Pos := + fun p => (A (M.vertIdx (Sum.inl p))).1 + +/-- The constraint of edge `(e, z, i)`, spelled out. -/ +theorem satisfies_toGraph_iff (A : M.toGraph.Assignment) (k : Fin (Fintype.card M.Edge)) : + M.toGraph.Satisfies A k ↔ + (M.check (M.edgeOf k).1 (M.edgeOf k).2.1 + (A (M.vertIdx (Sum.inr ((M.edgeOf k).1, (M.edgeOf k).2.1)))).2 = true + ∧ (A (M.vertIdx (Sum.inr ((M.edgeOf k).1, (M.edgeOf k).2.1)))).2 (M.edgeOf k).2.2 + = M.tableOf A (M.pos (M.edgeOf k).1 (M.edgeOf k).2.1 (M.edgeOf k).2.2)) := by + show decide _ = true ↔ _ + rw [decide_eq_true_iff] + rfl + +/-- The `Fin` index of an edge. -/ +noncomputable def edgeIdx (x : M.Edge) : Fin (Fintype.card M.Edge) := + NumEnc.equivFinCard M.Edge x + +omit [Fintype Pos] [NumEnc Pos] in +@[simp] theorem edgeOf_edgeIdx (x : M.Edge) : M.edgeOf (M.edgeIdx x) = x := + Equiv.symm_apply_apply _ _ + +/-- **A rejecting random string has a violated edge.** Either the claimed +answers fail the test, so every edge of the string is violated, or they pass and +therefore differ from the proof at some read. (With no reads there would be no +edges, so the test must read at least once.) -/ +theorem exists_unsat_of_rejects [Nonempty Q] (A : M.toGraph.Assignment) (e : E) + (z : Cube M.R) (hz : z ∈ M.rejects (M.tableOf A) e) : + ∃ i : Q, ¬ M.toGraph.Satisfies A (M.edgeIdx (e, z, i)) := by + classical + simp only [rejects, Finset.mem_filter, Finset.mem_univ, true_and] at hz + by_contra hall + push Not at hall + have hq' : ∀ i, (A (M.vertIdx (Sum.inr (e, z)))).2 i = M.tableOf A (M.pos e z i) := + fun i => by + have h := (M.satisfies_toGraph_iff A (M.edgeIdx (e, z, i))).1 (hall i) + simp only [edgeOf_edgeIdx] at h + exact h.2 + obtain ⟨i₀⟩ := ‹Nonempty Q› + have h := (M.satisfies_toGraph_iff A (M.edgeIdx (e, z, i₀))).1 (hall i₀) + simp only [edgeOf_edgeIdx] at h + have hcheck : M.check e z (A (M.vertIdx (Sum.inr (e, z)))).2 = true := h.1 + rw [funext hq'] at hcheck + have hz' : M.check e z (fun i => M.tableOf A (M.pos e z i)) = false := hz + rw [hz'] at hcheck + exact Bool.false_ne_true hcheck + +/-- A violated edge chosen for each rejecting string. -/ +noncomputable def witnessEdge [Nonempty Q] (A : M.toGraph.Assignment) + (x : E × Cube M.R) : Fin (Fintype.card M.Edge) := + if h : x.2 ∈ M.rejects (M.tableOf A) x.1 then + M.edgeIdx (x.1, x.2, Classical.choose (M.exists_unsat_of_rejects A x.1 x.2 h)) + else M.edgeIdx (x.1, x.2, Classical.arbitrary Q) + +theorem witnessEdge_mem [Nonempty Q] (A : M.toGraph.Assignment) (x : E × Cube M.R) + (hx : x.2 ∈ M.rejects (M.tableOf A) x.1) : + M.witnessEdge A x ∈ M.toGraph.unsatEdges A := by + classical + rw [witnessEdge, dif_pos hx] + exact Finset.mem_filter.2 + ⟨Finset.mem_univ _, Classical.choose_spec (M.exists_unsat_of_rejects A x.1 x.2 hx)⟩ + +theorem witnessEdge_injective [Nonempty Q] (A : M.toGraph.Assignment) : + Function.Injective (M.witnessEdge A) := by + intro x y hxy + have hx : M.edgeOf (M.witnessEdge A x) = M.edgeOf (M.witnessEdge A y) := by rw [hxy] + simp only [witnessEdge] at hx + split_ifs at hx <;> simp only [edgeOf_edgeIdx, Prod.mk.injEq] at hx <;> + exact Prod.ext hx.1 hx.2.1 + +/-- The set of rejecting (test, random string) pairs. -/ +def rejectPairs (T : Table Pos) : Finset (E × Cube M.R) := + Finset.univ.filter fun x => x.2 ∈ M.rejects T x.1 + +omit [Fintype Pos] [Fintype Q] [NumEnc Pos] [NumEnc E] [NumEnc Q] in +theorem card_rejectPairs (T : Table Pos) : + (M.rejectPairs T).card = ∑ e : E, (M.rejects T e).card := by + classical + rw [rejectPairs, Finset.card_filter, Fintype.sum_prod_type] + refine Finset.sum_congr rfl fun e _ => ?_ + rw [Finset.card_eq_sum_ones, ← Finset.sum_filter] + congr 1 + ext z + simp + +/-- **Rejections are counted by violated edges.** -/ +theorem card_rejects_le_card_unsatEdges [Nonempty Q] (A : M.toGraph.Assignment) : + ∑ e : E, (M.rejects (M.tableOf A) e).card ≤ (M.toGraph.unsatEdges A).card := by + classical + rw [← card_rejectPairs] + refine Finset.card_le_card_of_injOn (M.witnessEdge A) ?_ + (M.witnessEdge_injective A).injOn + intro x hx + have hx' : x.2 ∈ M.rejects (M.tableOf A) x.1 := by + have := Finset.mem_coe.1 hx + simpa [rejectPairs] using this + exact M.witnessEdge_mem A x hx' + +theorem numEdges_toGraph : M.toGraph.numEdges = Fintype.card E * 2 ^ M.R * Fintype.card Q := by + show Fintype.card (E × Cube M.R × Q) = _ + rw [Fintype.card_prod, Fintype.card_prod] + have hc : Fintype.card (Cube M.R) = 2 ^ M.R := by + show Fintype.card (Fin M.R → ZMod 2) = 2 ^ M.R + rw [Fintype.card_fun, ZMod.card, Fintype.card_fin] + rw [hc, mul_assoc] + +/-- **Soundness of the transformation.** The violated fraction of the binary +graph is at least the average rejection probability of the tests, divided by +the number of reads. -/ +theorem unsatFrac_toGraph_ge [Nonempty Q] (A : M.toGraph.Assignment) : + (∑ e : E, ((M.rejects (M.tableOf A) e).card : ℚ)) + / ((Fintype.card E : ℚ) * 2 ^ M.R * Fintype.card Q) + ≤ M.toGraph.unsatFrac A := by + classical + have h := M.card_rejects_le_card_unsatEdges A + have hE : (M.toGraph.numEdges : ℚ) + = (Fintype.card E : ℚ) * 2 ^ M.R * Fintype.card Q := by + rw [numEdges_toGraph]; push_cast; ring + rw [ConstraintGraph.unsatFrac, hE] + gcongr + exact_mod_cast h + +/-! ### Completeness -/ + +/-- The honest assignment: positions carry the proof, test vertices carry the +answers the proof gives. -/ +noncomputable def honest (T : Table Pos) : M.toGraph.Assignment := fun v => + match (NumEnc.equivFinCard M.Vert).symm v with + | Sum.inl p => (T p, fun _ => 0) + | Sum.inr (e, z) => (0, fun i => T (M.pos e z i)) + +theorem honest_inl (T : Table Pos) (p : Pos) : + M.honest T (M.vertIdx (Sum.inl p)) = (T p, fun _ => 0) := by + simp [honest, vertIdx] + +theorem honest_inr (T : Table Pos) (x : E × Cube M.R) : + M.honest T (M.vertIdx (Sum.inr x)) = (0, fun i => T (M.pos x.1 x.2 i)) := by + simp [honest, vertIdx] + +/-- **Completeness.** A proof accepted by every test on every random string +gives a satisfying assignment of the binary graph. -/ +theorem satisfiable_toGraph (T : Table Pos) (h : ∀ e z, M.accepts T e z = true) : + M.toGraph.Satisfiable := by + classical + refine ⟨M.honest T, fun k => ?_⟩ + rw [satisfies_toGraph_iff] + refine ⟨?_, ?_⟩ + · rw [honest_inr] + exact h _ _ + · rw [honest_inr] + show T _ = (M.honest T (M.vertIdx (Sum.inl _))).1 + rw [honest_inl] + +end Graph + +/-- The acceptance probability of a test, as a count of rejections. -/ +theorem prob_accepts_eq (T : Table Pos) (e : E) : + Pr[fun z : Cube M.R => M.accepts T e z = true] + = 1 - ((M.rejects T e).card : ℝ) / 2 ^ M.R := by + classical + rw [prob_eq_card_div] + have hsplit := Finset.card_filter_add_card_filter_not + (s := (Finset.univ : Finset (Cube M.R))) (fun z => M.accepts T e z = true) + have hrej : (M.rejects T e).card + = (Finset.univ.filter fun z => ¬ (M.accepts T e z = true)).card := by + congr 1 + ext z + simp [rejects] + rw [hrej] + have hcard : (Finset.univ : Finset (Cube M.R)).card = 2 ^ M.R := by + rw [Finset.card_univ] + show Fintype.card (Fin M.R → ZMod 2) = 2 ^ M.R + rw [Fintype.card_fun, ZMod.card, Fintype.card_fin] + rw [hcard] at hsplit + have hpos : (0 : ℝ) < 2 ^ M.R := by positivity + rw [eq_sub_iff_add_eq, ← add_div, div_eq_one_iff_eq hpos.ne'] + exact_mod_cast hsplit + +end MultiTest + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/Materialize.lean b/Complexitylib/Classes/PCP/Internal/Materialize.lean new file mode 100644 index 00000000..db8dba02 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/Materialize.lean @@ -0,0 +1,283 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.HeadOps +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.PCP.Internal.UnaryList +public import Complexitylib.Classes.PCP.Internal.ListEncode +public import Complexitylib.Classes.PCP.Internal.PositionsFP + +/-! +# Writing out a table + +Every stage of an algorithmic reduction writes a list: the edges of a graph, the +entries of a rotation table, the records of a gadget. `ListEncode` runs the loop +that does it, but asks for a bound on the loop's own state; this module +discharges that bound from the rule alone, since a polynomial-time rule has +polynomially bounded output and the loop runs no more often than its argument is +long. + +## Main results + +- `Complexity.materialize_mem_FP` — a record rule makes the list encoder + polynomial time +- `Complexity.materialize_eq` — and it writes the list it is meant to +- `Complexity.countOver`, `Complexity.length_countOver` — the same loop used to + add up a rule's outputs, which is how a bounded count is taken +- `Complexity.findFirst`, `Complexity.length_findFirst` — and, run twice, how a + bounded search is made +- `Complexity.ifEqLen`, `Complexity.ifLtLen` — comparing two unary numbers, and + branching on the answer +- `Complexity.length_findFirst_eq` — the search returns the least index the rule + answers at +-/ + +@[expose] public section + +namespace Complexity + +/-- **A record rule materializes a list in polynomial time.** No bound need be +supplied: a polynomial-time rule already has polynomially bounded output, and +the loop runs only as many times as its own argument is long. -/ +theorem materialize_mem_FP {E : List Bool → List Bool} (hE : E ∈ FP) : listEncFn E ∈ FP := by + obtain ⟨q, hq⟩ := Cobham.output_length_poly_of_mem_FP hE + set Q : Polynomial ℕ := q.comp (3 * Polynomial.X + Polynomial.C 2) with hQ + refine listEncFn_mem_FP hE + (4 * Polynomial.X * Q + 3 * Polynomial.X + Polynomial.C 6) fun z k hk => ?_ + have hfz : (pairFst z).length ≤ z.length := fstBlock_length_le z + have hsz : (pairSnd z).length ≤ z.length := pairSnd_length_le z + have hkz : k ≤ z.length := le_trans hk hfz + have hrec : ∀ i < k, (E (pair (pairSnd z) (List.replicate i true))).length + ≤ Q.eval z.length := by + intro i hi + refine le_trans (hq _) ?_ + have hlen : (pair (pairSnd z) (List.replicate i true)).length + ≤ 3 * z.length + 2 := by + rw [pair_length, List.length_replicate] + omega + have := polynomial_eval_mono_nat q hlen + rw [hQ, Polynomial.eval_comp] + simpa using this + have hcat : (entryCat E (pairSnd z) k).length ≤ k * Q.eval z.length := + length_entryCat_le E _ _ k hrec + have hstate : ((listStep E)^[k] (pair (pair [] []) (pairSnd z))).length + = 2 * (2 * (entryCat E (pairSnd z) k).length + 2 + k) + 2 + + (pairSnd z).length := by + rw [listStep_iterate, pair_length, pair_length, List.length_replicate] + have heval : (4 * Polynomial.X * Q + 3 * Polynomial.X + Polynomial.C 6).eval z.length + = 4 * (z.length * Q.eval z.length) + 3 * z.length + 6 := by + simp only [Polynomial.eval_add, Polynomial.eval_mul, Polynomial.eval_ofNat, + Polynomial.eval_X, Polynomial.eval_C] + ring + have hC : (entryCat E (pairSnd z) k).length ≤ z.length * Q.eval z.length := + le_trans hcat (Nat.mul_le_mul_right _ hkz) + rw [hstate, heval] + set A := z.length * Q.eval z.length with hA + omega + +/-- **The list encoder writes the list.** -/ +theorem materialize_eq {α : Type} [DataEncode α] {E : List Bool → List Bool} + (l : List α) (x : List Bool) + (h : ∀ i, ∀ hi : i < l.length, + E (pair x (List.replicate i true)) = DataEncode.bitstringEncode (l[i]'hi)) : + listEncFn E (pair (List.replicate l.length true) x) = DataEncode.bitstringEncode l := + listEncFn_eq_bitstringEncode l (by rw [pairFst_pair, List.length_replicate]) + (by rw [pairSnd_pair]; exact h) + +/-! ### Adding up -/ + +theorem length_entryCat (E : List Bool → List Bool) (x : List Bool) (n : ℕ) : + (entryCat E x n).length + = ∑ i ∈ Finset.range n, (E (pair x (List.replicate i true))).length := by + induction n with + | zero => simp + | succ n ih => + rw [entryCat_succ, List.length_append, ih, Finset.sum_range_succ] + +/-- The total length of a rule's outputs over a range, in unary. Running the +rule for its length alone is how a loop counts: a rule that answers `[true]` or +`[]` counts the indices where it says yes. -/ +noncomputable def countOver (E : List Bool → List Bool) (z : List Bool) : List Bool := + marks (dropOne (dropOne (listEncFn E z))) + +theorem countOver_mem_FP {E : List Bool → List Bool} (hE : E ∈ FP) : countOver E ∈ FP := + marks_mem_FP (dropOneFn_mem_FP (dropOneFn_mem_FP (materialize_mem_FP hE))) + +theorem length_countOver (E : List Bool → List Bool) (x : List Bool) (n : ℕ) : + (countOver E (pair (List.replicate n true) x)).length + = ∑ i ∈ Finset.range n, (E (pair x (List.replicate i true))).length := by + rw [countOver, marks_eq, List.length_replicate, dropOne, dropOne, List.length_drop, + List.length_drop, listEncFn_eq, pairFst_pair, pairSnd_pair, + List.length_replicate, List.length_append, List.length_cons, length_entryCat] + simp + +/-! ### Comparing -/ + +/-- `x` when the two strings have the same length, `y` otherwise. -/ +noncomputable def ifEqLen (a b x y : List Bool) : List Bool := + Cobham.selectHead (Cobham.emptyFlag (b.drop a.length ++ a.drop b.length)) x y + +theorem ifEqLen_pos {a b : List Bool} (h : a.length = b.length) (x y : List Bool) : + ifEqLen a b x y = x := by + have hb : b.drop a.length = [] := by + refine List.eq_nil_of_length_eq_zero ?_ + rw [List.length_drop, h] + omega + have ha : a.drop b.length = [] := by + refine List.eq_nil_of_length_eq_zero ?_ + rw [List.length_drop, h] + omega + rw [ifEqLen, hb, ha, List.append_nil, Cobham.selectHead_emptyFlag_nil] + +theorem ifEqLen_neg {a b : List Bool} (h : a.length ≠ b.length) (x y : List Bool) : + ifEqLen a b x y = y := by + have hne : b.drop a.length ++ a.drop b.length ≠ [] := by + intro hnil + have h1 := List.append_eq_nil_iff.mp hnil + have hb : (b.drop a.length).length = 0 := by rw [h1.1]; rfl + have ha : (a.drop b.length).length = 0 := by rw [h1.2]; rfl + rw [List.length_drop] at ha hb + omega + obtain ⟨c, t, hct⟩ : ∃ c t, b.drop a.length ++ a.drop b.length = c :: t := by + cases hcase : b.drop a.length ++ a.drop b.length with + | nil => exact absurd hcase hne + | cons c t => exact ⟨c, t, rfl⟩ + rw [ifEqLen, hct, Cobham.selectHead_emptyFlag_cons] + +theorem ifEqLen_mem_FP {a b x y : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) + (hx : x ∈ FP) (hy : y ∈ FP) : (fun z => ifEqLen (a z) (b z) (x z) (y z)) ∈ FP := by + have hd1 := dropLenFn_mem_FP ha hb + have hd2 := dropLenFn_mem_FP hb ha + exact Cobham.selectHeadFn_mem_FP + (Cobham.emptyFlag_mem_FP (Cobham.appendFn_mem_FP hd1 hd2)) hx hy + +/-- `x` when the first string is shorter than the second, `y` otherwise. -/ +noncomputable def ifLtLen (a b x y : List Bool) : List Bool := + Cobham.selectHead (Cobham.emptyFlag (b.drop a.length)) y x + +theorem ifLtLen_pos {a b : List Bool} (h : a.length < b.length) (x y : List Bool) : + ifLtLen a b x y = x := by + obtain ⟨c, t, hct⟩ : ∃ c t, b.drop a.length = c :: t := by + cases hcase : b.drop a.length with + | nil => + have : (b.drop a.length).length = 0 := by rw [hcase]; rfl + rw [List.length_drop] at this + omega + | cons c t => exact ⟨c, t, rfl⟩ + rw [ifLtLen, hct, Cobham.selectHead_emptyFlag_cons] + +theorem ifLtLen_neg {a b : List Bool} (h : ¬ a.length < b.length) (x y : List Bool) : + ifLtLen a b x y = y := by + have hb : b.drop a.length = [] := by + refine List.eq_nil_of_length_eq_zero ?_ + rw [List.length_drop] + omega + rw [ifLtLen, hb, Cobham.selectHead_emptyFlag_nil] + +theorem ifLtLen_mem_FP {a b x y : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) + (hx : x ∈ FP) (hy : y ∈ FP) : (fun z => ifLtLen (a z) (b z) (x z) (y z)) ∈ FP := + Cobham.selectHeadFn_mem_FP + (Cobham.emptyFlag_mem_FP (dropLenFn_mem_FP ha hb)) hy hx + +/-- The count is written in marks. -/ +theorem countOver_eq_replicate (E : List Bool → List Bool) (z : List Bool) : + countOver E z = List.replicate (countOver E z).length true := by + conv_lhs => rw [countOver, marks_eq] + rw [countOver, marks_eq, List.length_replicate] + +/-! ### Searching -/ + +/-- One mark when the string is empty, none otherwise. -/ +noncomputable def isEmptyMark (s : List Bool) : List Bool := + Cobham.selectHead (Cobham.emptyFlag s) [true] [] + +@[simp] theorem isEmptyMark_nil : isEmptyMark [] = [true] := + Cobham.selectHead_emptyFlag_nil _ _ + +@[simp] theorem isEmptyMark_cons (b : Bool) (t : List Bool) : isEmptyMark (b :: t) = [] := + Cobham.selectHead_emptyFlag_cons _ _ _ _ + +theorem length_isEmptyMark (s : List Bool) : + (isEmptyMark s).length = if s = [] then 1 else 0 := by + cases s with + | nil => simp + | cons b t => simp + +theorem isEmptyMark_mem_FP {f : List Bool → List Bool} (hf : f ∈ FP) : + (fun z => isEmptyMark (f z)) ∈ FP := + Cobham.selectHeadFn_mem_FP (Cobham.emptyFlag_mem_FP hf) (constFn_mem_FP [true]) + (constFn_mem_FP []) + +/-- The least index below the bound at which the rule answers something, or the +bound itself when it never does: count the indices no answer has been seen up +to. -/ +noncomputable def findFirst (E : List Bool → List Bool) (z : List Bool) : List Bool := + countOver (fun w => + isEmptyMark (countOver E (pair (pairSnd w ++ [true]) (pairFst w)))) z + +theorem findFirst_mem_FP {E : List Bool → List Bool} (hE : E ∈ FP) : findFirst E ∈ FP := by + have harg := Cobham.pairFn_mem_FP + (Cobham.appendFn_mem_FP Cobham.sndBlock_mem_FP (constFn_mem_FP [true])) + Cobham.fstBlock_mem_FP + have hcount : countOver E ∈ FP := countOver_mem_FP hE + have hcomp : (fun w : List Bool => + countOver E (pair (pairSnd w ++ [true]) (pairFst w))) ∈ FP := + mem_FP_of_eq (mem_FP_comp harg hcount) fun _ => rfl + exact countOver_mem_FP (isEmptyMark_mem_FP hcomp) + +/-- The search's answer is written in marks. -/ +theorem findFirst_eq_replicate (E : List Bool → List Bool) (z : List Bool) : + findFirst E z = List.replicate (findFirst E z).length true := by + conv_lhs => rw [findFirst, countOver_eq_replicate] + rw [← findFirst] + +theorem length_findFirst (E : List Bool → List Bool) (x : List Bool) (n : ℕ) : + (findFirst E (pair (List.replicate n true) x)).length + = ∑ j ∈ Finset.range n, + if (∑ k ∈ Finset.range (j + 1), (E (pair x (List.replicate k true))).length) = 0 + then 1 else 0 := by + rw [findFirst, length_countOver] + refine Finset.sum_congr rfl fun j _ => ?_ + rw [pairSnd_pair, pairFst_pair, ← List.replicate_succ', + length_isEmptyMark] + by_cases h : (∑ k ∈ Finset.range (j + 1), (E (pair x (List.replicate k true))).length) = 0 + · rw [if_pos h, if_pos] + have := length_countOver E x (j + 1) + exact List.eq_nil_of_length_eq_zero (by rw [this, h]) + · rw [if_neg h, if_neg] + intro hnil + exact h (by rw [← length_countOver E x (j + 1), hnil, List.length_nil]) + +/-- **The search returns the least index the rule answers at.** -/ +theorem length_findFirst_eq {E : List Bool → List Bool} {x : List Bool} {n c : ℕ} + (hc : c < n) (hhit : (E (pair x (List.replicate c true))).length ≠ 0) + (hmin : ∀ k < c, (E (pair x (List.replicate k true))).length = 0) : + (findFirst E (pair (List.replicate n true) x)).length = c := by + classical + rw [length_findFirst] + have hterm : ∀ j ∈ Finset.range n, + (if (∑ k ∈ Finset.range (j + 1), (E (pair x (List.replicate k true))).length) = 0 + then 1 else 0) + = (if j < c then 1 else 0) := by + intro j _ + by_cases hj : j < c + · rw [if_pos hj, if_pos] + refine Finset.sum_eq_zero fun k hk => ?_ + rw [Finset.mem_range] at hk + exact hmin k (by omega) + · rw [if_neg hj, if_neg] + intro hzero + refine hhit ?_ + have hcm : c ∈ Finset.range (j + 1) := Finset.mem_range.mpr (by omega) + exact (Finset.sum_eq_zero_iff.mp hzero) c hcm + rw [Finset.sum_congr rfl hterm, ← Finset.card_filter] + have hfilter : (Finset.range n).filter (fun j => j < c) = Finset.range c := by + ext j + simp only [Finset.mem_filter, Finset.mem_range] + omega + rw [hfilter, Finset.card_range] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/MaxLoop.lean b/Complexitylib/Classes/PCP/Internal/MaxLoop.lean new file mode 100644 index 00000000..6cfacac2 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/MaxLoop.lean @@ -0,0 +1,169 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.PolyRuler +public import Complexitylib.Classes.P.Cobham.Internal.HeadOps +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.PCP.Internal.UnaryDivMod +public import Complexitylib.Classes.PCP.Internal.PositionsFP + +/-! +# The largest of polynomially many values + +An algorithm reading a formula has to know how many variables it mentions, which +is the largest index any literal names. More generally: given a rule that +computes a value for each index, take the largest over a bounded range. + +Values are carried in unary, so "largest" is "longest", and the comparison is +the length test already in the toolkit. + +## Main definitions + +- `Complexity.maxStep` — one step of the running maximum +- `Complexity.maxOver` — the value it computes + +## Main results + +- `Complexity.maxStep_iterate` — the loop takes the maximum +- `Complexity.maxFn_mem_FP`, `Complexity.maxFn_eq` — the packaged loop +-/ + +@[expose] public section + +namespace Complexity + +/-- The largest of the first `n` values, as a length. -/ +def maxOver (f : List Bool → List Bool) (z : List Bool) : ℕ → ℕ + | 0 => 0 + | n + 1 => max (maxOver f z n) (f (pair z (List.replicate n true))).length + +/-- One step of the running maximum. The state is +`pair (pair largest counter) input`. -/ +def maxStep (f : List Bool → List Bool) (st : List Bool) : List Bool := + pair + (pair + (Cobham.selectHead + (Cobham.lenLeFlag (pairFst (pairFst st)) + (f (pair (pairSnd st) (pairSnd (pairFst st))))) + (pairFst (pairFst st)) + (f (pair (pairSnd st) (pairSnd (pairFst st))))) + (true :: pairSnd (pairFst st))) + (pairSnd st) + +theorem maxStep_mem_FP {f : List Bool → List Bool} (hf : f ∈ FP) : maxStep f ∈ FP := by + have hm : (fun st : List Bool => pairFst (pairFst st)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + have hi : (fun st : List Bool => pairSnd (pairFst st)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + have hz : (fun st : List Bool => pairSnd st) ∈ FP := Cobham.sndBlock_mem_FP + have hv : (fun st : List Bool => + f (pair (pairSnd st) (pairSnd (pairFst st)))) ∈ FP := by + have := mem_FP_comp (Cobham.pairFn_mem_FP hz hi) hf + simpa using this + exact Cobham.pairFn_mem_FP + (Cobham.pairFn_mem_FP + (Cobham.selectHeadFn_mem_FP (lenLeFlagFn_mem_FP hm hv) hm hv) + (mem_FP_comp hi (Cobham.cons_mem_FP true))) hz + +/-- **The loop takes the maximum.** -/ +theorem maxStep_iterate (f : List Bool → List Bool) (z : List Bool) : ∀ n : ℕ, + ∃ M, (maxStep f)^[n] (pair (pair [] []) z) + = pair (pair M (List.replicate n true)) z + ∧ M.length = maxOver f z n := by + intro n + induction n with + | zero => exact ⟨[], rfl, rfl⟩ + | succ n ih => + obtain ⟨M, hM, hlen⟩ := ih + rw [Function.iterate_succ_apply', hM, maxStep, pairFst_pair, + pairFst_pair, pairSnd_pair, pairSnd_pair] + set v := f (pair z (List.replicate n true)) with hv + rcases Cobham.lenLeFlag_flag M v with hf | hf + · rw [hf, selectHead_cons_true] + refine ⟨M, by rw [List.replicate_succ], ?_⟩ + rw [Cobham.lenLeFlag_eq_true_iff] at hf + rw [maxOver, ← hlen, ← hv] + omega + · rw [hf, selectHead_cons_false] + refine ⟨v, by rw [List.replicate_succ], ?_⟩ + have hgt : M.length < v.length := by + by_contra hcon + have : Cobham.lenLeFlag M v = [true] := + (Cobham.lenLeFlag_eq_true_iff M v).mpr (by omega) + rw [hf] at this + simp at this + rw [maxOver, ← hlen, ← hv] + omega + +/-- Every value is at most the maximum. -/ +theorem le_maxOver {f : List Bool → List Bool} {z : List Bool} : + ∀ (n i : ℕ), i < n → (f (pair z (List.replicate i true))).length ≤ maxOver f z n := by + intro n + induction n with + | zero => intro i hi; omega + | succ n ih => + intro i hi + rw [maxOver] + rcases Nat.lt_or_ge i n with h | h + · exact le_trans (ih i h) (le_max_left _ _) + · have : i = n := by omega + subst this + exact le_max_right _ _ + +theorem maxOver_le {f : List Bool → List Bool} {z : List Bool} {B : ℕ} : + ∀ n, (∀ i < n, (f (pair z (List.replicate i true))).length ≤ B) → maxOver f z n ≤ B := by + intro n + induction n with + | zero => intro _; simp [maxOver] + | succ n ih => + intro h + rw [maxOver, max_le_iff] + exact ⟨ih fun i hi => h i (by omega), h n (by omega)⟩ + +/-- **The packaged loop**, on `pair (unary count) input`. -/ +noncomputable def maxFn (f : List Bool → List Bool) (w : List Bool) : List Bool := + pairFst (pairFst ((maxStep f)^[(pairFst w).length] + (pair (pair [] []) (pairSnd w)))) + +theorem maxFn_mem_FP {f : List Bool → List Bool} (hf : f ∈ FP) : maxFn f ∈ FP := by + obtain ⟨pf, hpf⟩ := Cobham.output_length_poly_of_mem_FP hf + have hinit : (fun w : List Bool => pair (pair [] []) (pairSnd w)) ∈ FP := + Cobham.pairFn_mem_FP (constFn_mem_FP _) Cobham.sndBlock_mem_FP + set q : Polynomial ℕ := + Polynomial.C 4 * (pf.comp (Polynomial.C 3 * Polynomial.X + Polynomial.C 2)) + + Polynomial.C 3 * Polynomial.X + Polynomial.C 6 with hq + have hwidth : (fun w : List Bool => polyRuler q (id w)) ∈ FP := + polyRulerFn_mem_FP q id_mem_FP + have hbound : ∀ w : List Bool, ∀ k ≤ (pairFst w).length, + ((maxStep f)^[k] (pair (pair [] []) (pairSnd w))).length + ≤ (polyRuler q (id w)).length := by + intro w k hk + obtain ⟨M, hM, hlen⟩ := maxStep_iterate f (pairSnd w) k + have hfw : (pairFst w).length ≤ w.length := fstBlock_length_le w + have hzw : (pairSnd w).length ≤ w.length := pairSnd_length_le w + have hMB : M.length ≤ pf.eval (3 * w.length + 2) := by + rw [hlen] + refine maxOver_le k fun i hi => ?_ + refine le_trans (hpf _) (polynomial_eval_mono_nat pf ?_) + rw [pair_length, List.length_replicate] + omega + rw [hM, pair_length, pair_length, polyRuler_length, List.length_replicate, hq] + simp only [Polynomial.eval_add, Polynomial.eval_mul, Polynomial.eval_C, + Polynomial.eval_X, Polynomial.eval_comp, id] + omega + have hiter := Cobham.iterate_mem_FP (maxStep_mem_FP hf) hinit + Cobham.fstBlock_mem_FP hwidth hbound + have := mem_FP_comp (mem_FP_comp hiter Cobham.fstBlock_mem_FP) Cobham.fstBlock_mem_FP + simpa using this + +theorem maxFn_eq (f : List Bool → List Bool) {n : ℕ} {z : List Bool} : + (maxFn f (pair (List.replicate n true) z)).length = maxOver f z n := by + obtain ⟨M, hM, hlen⟩ := maxStep_iterate f z n + rw [maxFn, pairFst_pair, pairSnd_pair, List.length_replicate, hM, + pairFst_pair, pairFst_pair, hlen] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/MergeGen.lean b/Complexitylib/Classes/PCP/Internal/MergeGen.lean new file mode 100644 index 00000000..759909b0 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/MergeGen.lean @@ -0,0 +1,486 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.ExpanderMerge +public import Mathlib.Data.Real.Sqrt + +/-! +# Merging at an arbitrary width + +`ExpanderMerge` folds `N ≤ 3 n` vertices onto `n`. The zig-zag tower produces +sizes far more widely spaced than that, so the fold has to work at any width: +`N` vertices onto `n`, with `(m - 1) n ≤ N ≤ m n`. + +What keeps the estimate under control at every width is that the fibres stay +balanced — `ExpanderMerge.card_liftN_none_le_one` — so however large `m` is, +each new vertex needs at most one padding loop. + +This module carries the combinatorial layer: the rotation map and the graph. + +## Main definitions + +- `Complexity.RegGraph.mergeRotN` — the rotation map of the wide merge +- `Complexity.RegGraph.mergedN` — the merged graph + +## Main results + +- `Complexity.RegGraph.mergeRotN_involutive` +- `Complexity.RegGraph.order_mergedN`, `Complexity.RegGraph.deg_mergedN` +- `Complexity.RegGraph.step_mergedN` — the merged walk averages the `m` slots +- `Complexity.RegGraph.sq_step_mergedN_le` — and Jensen bounds its square +- `Complexity.RegGraph.sum_over_liftN` — the filled slots enumerate the old + vertices +- `Complexity.RegGraph.sum_sq_termN_le` — the split into old steps and padding +- `Complexity.RegGraph.spectralBound_mergedN` — **the spectral bound at any + width** +- `Complexity.mergeWidth` — a width that always works +-/ + +@[expose] public section + +namespace Complexity + +namespace RegGraph + +variable {N d n m : ℕ} + +/-- The new vertex an old one folds onto. -/ +def projN (n : ℕ) (hn : 0 < n) (u : Fin N) : Fin n := ⟨u.val % n, Nat.mod_lt _ hn⟩ + +/-- Which slot of its fibre an old vertex occupies. -/ +def slotN (n m : ℕ) (hN : N ≤ m * n) (u : Fin N) : Fin m := + ⟨u.val / n, by + rcases Nat.eq_zero_or_pos n with h0 | h0 + · subst h0 + have := u.isLt + omega + · have := u.isLt + rw [Nat.div_lt_iff_lt_mul h0] + omega⟩ + +theorem liftN_projN_slotN (hn : 0 < n) (hN : N ≤ m * n) (u : Fin N) : + liftN N n (projN n hn u) (slotN n m hN u).val = some u := by + rw [liftN, projN, slotN] + have h : u.val % n + u.val / n * n = u.val := by + rw [mul_comm] + exact Nat.mod_add_div u.val n + rw [dif_pos (by rw [h]; exact u.isLt)] + congr 1 + exact Fin.ext h + +theorem projN_liftN (hn : 0 < n) (v : Fin n) (i : ℕ) (u : Fin N) + (h : liftN N n v i = some u) : projN n hn u = v := by + rw [liftN] at h + split_ifs at h with hlt + · simp only [Option.some.injEq] at h + rw [← h, projN] + apply Fin.ext + show (v.val + i * n) % n = v.val + rw [Nat.add_mul_mod_self_right, Nat.mod_eq_of_lt v.isLt] + +theorem slotN_liftN (hn : 0 < n) (hN : N ≤ m * n) (v : Fin n) (i : ℕ) (u : Fin N) + (h : liftN N n v i = some u) : (slotN n m hN u).val = i := by + rw [liftN] at h + split_ifs at h with hlt + · simp only [Option.some.injEq] at h + rw [← h, slotN] + show (v.val + i * n) / n = i + rw [Nat.add_mul_div_right _ _ hn, Nat.div_eq_of_lt v.isLt, zero_add] + +/-- The rotation map of the wide merge. -/ +def mergeRotN (hn : 0 < n) (hN : N ≤ m * n) (rot : Fin N × Fin d → Fin N × Fin d) + (x : Fin n × (Fin m × Fin d)) : Fin n × (Fin m × Fin d) := + match liftN N n x.1 x.2.1.val with + | some u => + let y := rot (u, x.2.2) + (projN n hn y.1, (slotN n m hN y.1, y.2)) + | none => x + +theorem mergeRotN_involutive (hn : 0 < n) (hN : N ≤ m * n) + (rot : Fin N × Fin d → Fin N × Fin d) (hrot : Function.Involutive rot) : + Function.Involutive (mergeRotN hn hN rot) := by + intro x + obtain ⟨v, i, s⟩ := x + simp only [mergeRotN] + cases hl : liftN N n v i.val with + | none => simp [hl] + | some u => + simp only + have hs : (slotN n m hN u).val = i.val := slotN_liftN hn hN v i.val u hl + have hslot : slotN n m hN u = i := Fin.ext hs + rw [liftN_projN_slotN hn hN] + simp only + rcases hrs : rot (u, s) with ⟨u', s'⟩ + have hy : rot (u', s') = (u, s) := by + rw [← hrs] + exact hrot (u, s) + simp only [hy, Prod.mk.injEq] + exact ⟨projN_liftN hn v i.val u hl, hslot, trivial⟩ + +/-- **The wide merge.** -/ +def mergedN (hn : 0 < n) (hd : 0 < d) (hm : 0 < m) (hN : N ≤ m * n) + (rot : Fin N × Fin d → Fin N × Fin d) (hrot : Function.Involutive rot) : RegGraph where + V := Fin n + D := Fin m × Fin d + decEqV := inferInstance + decEqD := inferInstance + fintypeV := inferInstance + fintypeD := inferInstance + nonemptyD := ⟨(⟨0, hm⟩, ⟨0, hd⟩)⟩ + rot := mergeRotN hn hN rot + rot_involutive := mergeRotN_involutive hn hN rot hrot + +@[simp] theorem order_mergedN (hn : 0 < n) (hd : 0 < d) (hm : 0 < m) (hN : N ≤ m * n) + (rot : Fin N × Fin d → Fin N × Fin d) (hrot : Function.Involutive rot) : + (mergedN hn hd hm hN rot hrot).order = n := Fintype.card_fin n + +@[simp] theorem deg_mergedN (hn : 0 < n) (hd : 0 < d) (hm : 0 < m) (hN : N ≤ m * n) + (rot : Fin N × Fin d → Fin N × Fin d) (hrot : Function.Involutive rot) : + (mergedN hn hd hm hN rot hrot).deg = m * d := by + show Fintype.card (Fin m × Fin d) = m * d + rw [Fintype.card_prod, Fintype.card_fin, Fintype.card_fin] + +/-! ### The walk of the wide merge -/ + +/-- A term of the merged step at `v`: the old step at the vertex in slot `i`, or +`f v` where the slot is empty. -/ +noncomputable def termN (hn : 0 < n) (hd : 0 < d) + (rot : Fin N × Fin d → Fin N × Fin d) (hrot : Function.Involutive rot) + (f : Fin n → ℝ) (v : Fin n) (i : ℕ) : ℝ := + match liftN N n v i with + | some u => (base hd rot hrot).step (fun w => f (projN n hn w)) u + | none => f v + +/-- **The merged walk is the average over the slots.** -/ +theorem step_mergedN (hn : 0 < n) (hd : 0 < d) (hm : 0 < m) (hN : N ≤ m * n) + (rot : Fin N × Fin d → Fin N × Fin d) (hrot : Function.Involutive rot) + (f : Fin n → ℝ) (v : Fin n) : + (mergedN hn hd hm hN rot hrot).step f v + = (∑ i ∈ Finset.range m, termN hn hd rot hrot f v i) / (m : ℝ) := by + have hd' : (d : ℝ) ≠ 0 := by + have : (0 : ℝ) < d := by exact_mod_cast hd + exact ne_of_gt this + have hm' : (m : ℝ) ≠ 0 := by + have : (0 : ℝ) < m := by exact_mod_cast hm + exact ne_of_gt this + have hdeg : ((mergedN hn hd hm hN rot hrot).deg : ℝ) = (m : ℝ) * (d : ℝ) := by + rw [deg_mergedN] + push_cast + ring + have hsum : (∑ x : (mergedN hn hd hm hN rot hrot).D, + f ((mergedN hn hd hm hN rot hrot).nbr v x)) + = ∑ i : Fin m, ∑ s : Fin d, f ((mergeRotN hn hN rot (v, (i, s))).1) := + Fintype.sum_prod_type + (f := fun x : Fin m × Fin d => f ((mergeRotN hn hN rot (v, x)).1)) + rw [RegGraph.step, hdeg, hsum] + have hinner : ∀ i : Fin m, ∑ s : Fin d, f ((mergeRotN hn hN rot (v, (i, s))).1) + = (d : ℝ) * termN hn hd rot hrot f v i.val := by + intro i + rw [termN] + simp only [mergeRotN] + cases hl : liftN N n v i.val with + | none => + simp only [Finset.sum_const, Finset.card_univ, Fintype.card_fin, nsmul_eq_mul] + | some u => + simp only + rw [RegGraph.step, deg_ofRot] + show _ = (d : ℝ) * ((∑ j : Fin d, f (projN n hn (rot (u, j)).1)) / (d : ℝ)) + field_simp + rw [Finset.sum_congr rfl fun i _ => hinner i, ← Finset.mul_sum] + rw [Fin.sum_univ_eq_sum_range (fun i => termN hn hd rot hrot f v i) m] + field_simp + +/-- **Jensen**: the square of the average is at most the average of the +squares. -/ +theorem sq_step_mergedN_le (hn : 0 < n) (hd : 0 < d) (hm : 0 < m) (hN : N ≤ m * n) + (rot : Fin N × Fin d → Fin N × Fin d) (hrot : Function.Involutive rot) + (f : Fin n → ℝ) (v : Fin n) : + ((mergedN hn hd hm hN rot hrot).step f v) ^ 2 + ≤ (∑ i ∈ Finset.range m, (termN hn hd rot hrot f v i) ^ 2) / (m : ℝ) := by + have hm' : (0 : ℝ) < m := by exact_mod_cast hm + rw [step_mergedN, div_pow] + have h := sq_sum_le_card_mul_sum_sq (s := Finset.range m) + (f := fun i => termN hn hd rot hrot f v i) + rw [Finset.card_range] at h + rw [div_le_div_iff₀ (by positivity) hm'] + nlinarith [h, hm'] + +/-! ### Summing over the slots -/ + +/-- **The filled slots are exactly the old vertices.** -/ +theorem sum_over_liftN (hn : 0 < n) (hN : N ≤ m * n) (g : Fin N → ℝ) : + ∑ p : Fin n × Fin m, + (match liftN N n p.1 p.2.val with | some u => g u | none => 0) + = ∑ u : Fin N, g u := by + classical + have hinj : Function.Injective fun u : Fin N => (projN n hn u, slotN n m hN u) := by + intro u u' h + have h1 := liftN_projN_slotN hn hN u + have h2 := liftN_projN_slotN hn hN u' + simp only [Prod.mk.injEq] at h + rw [h.1, h.2, h2] at h1 + exact (Option.some.inj h1).symm + symm + calc ∑ u : Fin N, g u + = ∑ u : Fin N, (match liftN N n (projN n hn u) (slotN n m hN u).val with + | some u' => g u' | none => 0) := by + refine Finset.sum_congr rfl fun u _ => ?_ + rw [liftN_projN_slotN hn hN] + _ = ∑ p ∈ Finset.univ.image (fun u : Fin N => (projN n hn u, slotN n m hN u)), + (match liftN N n p.1 p.2.val with | some u' => g u' | none => 0) := by + rw [Finset.sum_image (fun u _ u' _ h => hinj h)] + _ = ∑ p : Fin n × Fin m, + (match liftN N n p.1 p.2.val with | some u' => g u' | none => 0) := by + refine Finset.sum_subset (Finset.subset_univ _) fun p _ hp => ?_ + cases hl : liftN N n p.1 p.2.val with + | none => rfl + | some u => + exfalso + apply hp + rw [Finset.mem_image] + refine ⟨u, Finset.mem_univ _, ?_⟩ + have h1 := projN_liftN hn p.1 p.2.val u hl + have h2 := slotN_liftN hn hN p.1 p.2.val u hl + exact Prod.ext h1 (Fin.ext h2) + +/-- **Splitting the slot terms**: the old steps, plus at most one padding per +vertex — this is where balance of the fibres is used. -/ +theorem sum_sq_termN_le (hn : 0 < n) (hd : 0 < d) (hN : N ≤ m * n) + (hm1 : (m - 1) * n ≤ N) (rot : Fin N × Fin d → Fin N × Fin d) + (hrot : Function.Involutive rot) (f : Fin n → ℝ) : + ∑ v : Fin n, ∑ i : Fin m, (termN hn hd rot hrot f v i.val) ^ 2 + ≤ (∑ u : Fin N, ((base hd rot hrot).step (fun w => f (projN n hn w)) u) ^ 2) + + ∑ v : Fin n, (f v) ^ 2 := by + classical + have hsplit : ∀ (v : Fin n) (i : Fin m), (termN hn hd rot hrot f v i.val) ^ 2 + = (match liftN N n v i.val with + | some u => ((base hd rot hrot).step (fun w => f (projN n hn w)) u) ^ 2 + | none => 0) + + (match liftN N n v i.val with | some _ => 0 | none => (f v) ^ 2) := by + intro v i + simp only [termN] + cases liftN N n v i.val <;> simp + simp_rw [hsplit, Finset.sum_add_distrib] + rw [← Fintype.sum_prod_type', sum_over_liftN hn hN] + refine add_le_add le_rfl (Finset.sum_le_sum fun v _ => ?_) + -- at most one empty slot per vertex + have hone : ∑ i : Fin m, + (match liftN N n v i.val with | some _ => (0 : ℝ) | none => (f v) ^ 2) + ≤ (f v) ^ 2 := by + have hcard := card_liftN_none_le_one (N := N) (n := n) (m := m) hm1 v + have hstep : ∑ i : Fin m, + (match liftN N n v i.val with | some _ => (0 : ℝ) | none => (f v) ^ 2) + = ∑ i ∈ (Finset.range m).filter fun i => liftN N n v i = none, (f v) ^ 2 := by + have hconv : ∑ i : Fin m, + (match liftN N n v i.val with | some _ => (0 : ℝ) | none => (f v) ^ 2) + = ∑ i ∈ Finset.range m, + (match liftN N n v i with | some _ => (0 : ℝ) | none => (f v) ^ 2) := + Fin.sum_univ_eq_sum_range + (fun i => (match liftN N n v i with | some _ => (0 : ℝ) | none => (f v) ^ 2)) m + rw [hconv, Finset.sum_filter] + refine Finset.sum_congr rfl fun i _ => ?_ + cases liftN N n v i <;> simp + rw [hstep, Finset.sum_const, nsmul_eq_mul] + have hc : ((((Finset.range m).filter fun i => liftN N n v i = none).card : ℕ) : ℝ) ≤ 1 := by + exact_mod_cast hcard + nlinarith [hc, sq_nonneg (f v)] + exact hone + +/-! ### The spectral bound at any width -/ + +/-- The wide projection is the one `ExpanderMerge` already used. -/ +theorem projN_eq_proj (hn : 0 < n) : projN (N := N) n hn = proj n hn := rfl + +theorem sum_sq_liftN_le (hn : 0 < n) (hN : N ≤ m * n) (f : Fin n → ℝ) : + ∑ u : Fin N, (f (projN n hn u)) ^ 2 ≤ (m : ℝ) * ∑ v : Fin n, (f v) ^ 2 := by + classical + rw [← sum_over_liftN hn hN (fun u => (f (projN n hn u)) ^ 2), Fintype.sum_prod_type, + Finset.mul_sum] + refine Finset.sum_le_sum fun v _ => ?_ + have hterm : ∀ i : Fin m, + (match liftN N n v i.val with | some u => (f (projN n hn u)) ^ 2 | none => 0) + ≤ (f v) ^ 2 := by + intro i + cases hl : liftN N n v i.val with + | none => show (0 : ℝ) ≤ (f v) ^ 2; exact sq_nonneg _ + | some u => + show (f (projN n hn u)) ^ 2 ≤ (f v) ^ 2 + rw [projN_liftN hn v i.val u hl] + calc ∑ i : Fin m, + (match liftN N n v i.val with | some u => (f (projN n hn u)) ^ 2 | none => 0) + ≤ ∑ _i : Fin m, (f v) ^ 2 := Finset.sum_le_sum fun i _ => hterm i + _ = (m : ℝ) * (f v) ^ 2 := by + rw [Finset.sum_const, Finset.card_univ, Fintype.card_fin, nsmul_eq_mul] + +/-- **The lift's total**, for a mean-zero `f`: only the last slot survives. -/ +theorem sum_liftN_eq (hn : 0 < n) (hN : N ≤ m * n) (hm1 : (m - 1) * n ≤ N) (hm : 0 < m) + (f : Fin n → ℝ) (hf : ∑ v : Fin n, f v = 0) : + ∑ u : Fin N, f (projN n hn u) + = ∑ v : Fin n, (match liftN N n v (m - 1) with | some _ => f v | none => 0) := by + classical + rw [← sum_over_liftN hn hN (fun u => f (projN n hn u)), Fintype.sum_prod_type] + have hv : ∀ v : Fin n, ∑ i : Fin m, + (match liftN N n v i.val with | some u => f (projN n hn u) | none => 0) + = ((m : ℝ) - 1) * f v + + (match liftN N n v (m - 1) with | some _ => f v | none => 0) := by + intro v + have hval : ∀ i : Fin m, + (match liftN N n v i.val with | some u => f (projN n hn u) | none => 0) + = (match liftN N n v i.val with | some _ => f v | none => 0) := by + intro i + cases hl : liftN N n v i.val with + | none => rfl + | some u => + show f (projN n hn u) = f v + rw [projN_liftN hn v i.val u hl] + rw [Finset.sum_congr rfl fun i _ => hval i] + have hsplit : ∀ i : Fin m, + (match liftN N n v i.val with | some _ => f v | none => 0) + = f v - (match liftN N n v i.val with | some _ => 0 | none => f v) := by + intro i + cases liftN N n v i.val <;> simp + rw [Finset.sum_congr rfl fun i _ => hsplit i, Finset.sum_sub_distrib, + Finset.sum_const, Finset.card_univ, Fintype.card_fin, nsmul_eq_mul] + have hnone : ∑ i : Fin m, + (match liftN N n v i.val with | some _ => (0 : ℝ) | none => f v) + = (match liftN N n v (m - 1) with | some _ => (0 : ℝ) | none => f v) := by + have hconv : ∑ i : Fin m, + (match liftN N n v i.val with | some _ => (0 : ℝ) | none => f v) + = ∑ i ∈ Finset.range m, + (match liftN N n v i with | some _ => (0 : ℝ) | none => f v) := + Fin.sum_univ_eq_sum_range + (fun i => (match liftN N n v i with | some _ => (0 : ℝ) | none => f v)) m + rw [hconv, Finset.sum_eq_single (m - 1)] + · intro b hb hbne + rw [Finset.mem_range] at hb + have : b + 1 < m := by omega + have hsome := liftN_isSome hm1 v this + cases hl : liftN N n v b with + | none => rw [hl] at hsome; exact absurd hsome (by simp) + | some _ => rfl + · intro hcon + exact absurd (Finset.mem_range.2 (by omega)) hcon + rw [hnone] + cases liftN N n v (m - 1) <;> · simp; ring + rw [Finset.sum_congr rfl fun v _ => hv v, Finset.sum_add_distrib, ← Finset.mul_sum, hf, + mul_zero, zero_add] + +/-- **The mean of the lift is small.** -/ +theorem sq_sum_liftN_le (hn : 0 < n) (hN : N ≤ m * n) (hm1 : (m - 1) * n ≤ N) (hm : 0 < m) + (f : Fin n → ℝ) (hf : ∑ v : Fin n, f v = 0) : + (∑ u : Fin N, f (projN n hn u)) ^ 2 ≤ (n : ℝ) * ∑ v : Fin n, (f v) ^ 2 := by + classical + rw [sum_liftN_eq hn hN hm1 hm f hf] + set Hv : Finset (Fin n) := + Finset.univ.filter fun v => (liftN N n v (m - 1)).isSome with hH + have hsum : ∑ v : Fin n, (match liftN N n v (m - 1) with | some _ => f v | none => 0) + = ∑ v ∈ Hv, f v := by + rw [hH, Finset.sum_filter] + refine Finset.sum_congr rfl fun v _ => ?_ + cases liftN N n v (m - 1) <;> simp + rw [hsum] + have hcs := sq_sum_le_card_mul_sum_sq (s := Hv) (f := f) + have hcard : (Hv.card : ℝ) ≤ (n : ℝ) := by + have : Hv.card ≤ Fintype.card (Fin n) := Finset.card_le_univ Hv + rw [Fintype.card_fin] at this + exact_mod_cast this + have hsub : ∑ v ∈ Hv, (f v) ^ 2 ≤ ∑ v : Fin n, (f v) ^ 2 := + Finset.sum_le_sum_of_subset_of_nonneg (Finset.subset_univ _) fun v _ _ => sq_nonneg _ + have h0 : 0 ≤ ∑ v ∈ Hv, (f v) ^ 2 := Finset.sum_nonneg fun v _ => sq_nonneg _ + calc (∑ v ∈ Hv, f v) ^ 2 ≤ Hv.card * ∑ v ∈ Hv, (f v) ^ 2 := hcs + _ ≤ (n : ℝ) * ∑ v : Fin n, (f v) ^ 2 := by + exact mul_le_mul hcard hsub h0 (by positivity) + +/-- **The spectral bound of the wide merge.** The width enters only through the +`1 / m` terms, so the bound stays below one however far apart the sizes are. -/ +theorem spectralBound_mergedN (hn : 0 < n) (hd : 0 < d) (hm : 0 < m) (hN : N ≤ m * n) + (hm1 : (m - 1) * n ≤ N) (h2 : 2 * n ≤ N) + (rot : Fin N × Fin d → Fin N × Fin d) (hrot : Function.Involutive rot) + {lam : ℝ} (hlam : lam ^ 2 ≤ 1) (hspec : (base hd rot hrot).SpectralBound lam) : + (mergedN hn hd hm hN rot hrot).SpectralBound + (Real.sqrt (lam ^ 2 + (1 - lam ^ 2) / (2 * m) + 1 / m)) := by + intro f hf + have hN0 : 0 < N := by omega + have hN' : (0 : ℝ) < N := by exact_mod_cast hN0 + have hm' : (0 : ℝ) < m := by exact_mod_cast hm + have hl1 : 0 ≤ 1 - lam ^ 2 := by linarith + have hl0 : 0 ≤ lam ^ 2 := sq_nonneg _ + have hsq : Real.sqrt (lam ^ 2 + (1 - lam ^ 2) / (2 * m) + 1 / m) ^ 2 + = lam ^ 2 + (1 - lam ^ 2) / (2 * m) + 1 / m := + Real.sq_sqrt (by positivity) + rw [hsq] + have hf' : ∑ v : Fin n, f v = 0 := hf + have hS : 0 ≤ ∑ v : Fin n, (f v) ^ 2 := Finset.sum_nonneg fun _ _ => sq_nonneg _ + have hjensen : ∑ v : Fin n, ((mergedN hn hd hm hN rot hrot).step f v) ^ 2 + ≤ (∑ v : Fin n, ∑ i ∈ Finset.range m, (termN hn hd rot hrot f v i) ^ 2) / (m : ℝ) := by + rw [Finset.sum_div] + exact Finset.sum_le_sum fun v _ => sq_step_mergedN_le hn hd hm hN rot hrot f v + have hconv : ∀ v : Fin n, ∑ i ∈ Finset.range m, (termN hn hd rot hrot f v i) ^ 2 + = ∑ i : Fin m, (termN hn hd rot hrot f v i.val) ^ 2 := fun v => + (Fin.sum_univ_eq_sum_range (fun i => (termN hn hd rot hrot f v i) ^ 2) m).symm + rw [Finset.sum_congr rfl fun v _ => hconv v] at hjensen + have hterms := sum_sq_termN_le hn hd hN hm1 rot hrot f + have hold := sum_sq_step_lift_le hn hd rot hrot hspec hN0 f + rw [← projN_eq_proj hn] at hold + have hlift := sum_sq_liftN_le hn hN f + have hmean := sq_sum_liftN_le hn hN hm1 hm f hf' + have hmeanN : (∑ u : Fin N, f (projN n hn u)) ^ 2 / (N : ℝ) + ≤ (1 / 2) * ∑ v : Fin n, (f v) ^ 2 := by + rw [div_le_iff₀ hN'] + have h2' : (2 : ℝ) * n ≤ N := by exact_mod_cast h2 + nlinarith [hmean, h2', hS] + have hA := mul_le_mul_of_nonneg_left hlift hl0 + have hB := mul_le_mul_of_nonneg_left hmeanN hl1 + have hT : ∑ v : Fin n, ∑ i : Fin m, (termN hn hd rot hrot f v i.val) ^ 2 + ≤ (lam ^ 2 * (m : ℝ) + (1 - lam ^ 2) / 2 + 1) * ∑ v : Fin n, (f v) ^ 2 := by + nlinarith [hterms, hold, hA, hB, hS] + have hfin : (∑ v : Fin n, ∑ i : Fin m, (termN hn hd rot hrot f v i.val) ^ 2) / (m : ℝ) + ≤ (lam ^ 2 + (1 - lam ^ 2) / (2 * m) + 1 / m) * ∑ v : Fin n, (f v) ^ 2 := by + rw [div_le_iff₀ hm'] + have hexp : (lam ^ 2 + (1 - lam ^ 2) / (2 * m) + 1 / m) + * (∑ v : Fin n, (f v) ^ 2) * (m : ℝ) + = (lam ^ 2 * (m : ℝ) + (1 - lam ^ 2) / 2 + 1) * ∑ v : Fin n, (f v) ^ 2 := by + field_simp + rw [hexp] + exact hT + show ∑ v : Fin n, ((mergedN hn hd hm hN rot hrot).step f v) ^ 2 ≤ _ + exact le_trans hjensen hfin + +/-! ### Choosing the width -/ + +/-- A width that always works for folding `N ≥ 2 n` vertices onto `n`. -/ +def mergeWidth (N n : ℕ) : ℕ := N / n + 1 + +theorem le_mergeWidth_mul (N : ℕ) {n : ℕ} (hn : 0 < n) : N ≤ mergeWidth N n * n := by + rw [mergeWidth] + have h := Nat.div_add_mod N n + have hlt := Nat.mod_lt N hn + calc N = n * (N / n) + N % n := h.symm + _ ≤ n * (N / n) + n := by omega + _ = (N / n + 1) * n := by ring + +theorem mergeWidth_sub_one_mul_le (N n : ℕ) : (mergeWidth N n - 1) * n ≤ N := by + rw [mergeWidth, Nat.add_sub_cancel] + exact Nat.div_mul_le_self N n + +theorem three_le_mergeWidth {N n : ℕ} (hn : 0 < n) (h2 : 2 * n ≤ N) : + 3 ≤ mergeWidth N n := by + rw [mergeWidth] + have h : 2 ≤ N / n := (Nat.le_div_iff_mul_le hn).2 (by omega) + omega + +/-- The width is bounded whenever the overshoot is. -/ +theorem mergeWidth_le {N n C : ℕ} (hn : 0 < n) (h : N ≤ C * n) : + mergeWidth N n ≤ C + 1 := by + rw [mergeWidth] + have : N / n ≤ C := (Nat.div_le_iff_le_mul_add_pred hn).2 (by + have : C * n = n * C := by ring + omega) + omega + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/Mixing.lean b/Complexitylib/Classes/PCP/Internal/Mixing.lean new file mode 100644 index 00000000..62359ca5 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/Mixing.lean @@ -0,0 +1,188 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.RegularGraph + +/-! +# The expander mixing lemma for `t`-step walks + +The quantitative heart of every expander argument, in the square-norm form set +up in `RegularGraph`: on a graph with `SpectralBound lam`, the correlation +between a function `f` at the start of a `t`-step walk and a function `g` at its +end is what independence would predict, up to `lam ^ t` times the two standard +deviations. + +Stated with everything squared, so no `Real.sqrt` appears: + +`(⟪f, Aᵗ g⟫ - (∑ f)(∑ g)/n) ^ 2 ≤ lam ^ (2t) · Var f · Var g` + +where `Var f = ∑ f² - (∑ f)²/n` is the (unnormalised) variance. Specialised to +indicator functions of vertex sets this is the usual expander mixing lemma, and +it is the estimate Dinur's powering step applies to the sets of vertices whose +walk-labels disagree with a global assignment. + +## Main definitions + +- `RegGraph.mean`, `RegGraph.center` — the mean of a function and its + mean-zero part + +## Main results + +- `RegGraph.step_const`, `RegGraph.step_add`, `RegGraph.stepIter_const`, + `RegGraph.stepIter_add` — the walk operator is affine-linear and fixes + constants +- `RegGraph.sum_center`, `RegGraph.sum_sq_center` — the Pythagoras identity + splitting a function into its mean and its mean-zero part +- `RegGraph.inner_stepIter_eq` — the correlation splits into the independent + part and a mean-zero correlation +- `RegGraph.mixing_sq` — the mixing lemma +-/ + +@[expose] public section + +namespace Complexity + +namespace RegGraph + +variable (G : RegGraph) + +/-! ### Linearity of the walk operator -/ + +@[simp] theorem step_const (c : ℝ) (v : G.V) : G.step (fun _ => c) v = c := by + have hd : (G.deg : ℝ) ≠ 0 := G.deg_ne_zero + simp only [step, Finset.sum_const, Finset.card_univ, nsmul_eq_mul, card_eq_deg] + field_simp + +theorem step_add (f g : G.V → ℝ) (v : G.V) : + G.step (fun w => f w + g w) v = G.step f v + G.step g v := by + simp only [step] + rw [← add_div, Finset.sum_add_distrib] + +theorem stepIter_const (t : ℕ) (c : ℝ) : G.stepIter t (fun _ => c) = fun _ => c := by + induction t with + | zero => rfl + | succ t ih => rw [stepIter_succ, ih]; funext v; exact G.step_const c v + +theorem stepIter_add (t : ℕ) (f g : G.V → ℝ) : + G.stepIter t (fun w => f w + g w) = fun v => G.stepIter t f v + G.stepIter t g v := by + induction t with + | zero => rfl + | succ t ih => + rw [stepIter_succ, ih] + funext v + rw [G.step_add, ← stepIter_succ, ← stepIter_succ] + +/-! ### Centering -/ + +/-- The mean of a function on the vertices. -/ +noncomputable def mean (f : G.V → ℝ) : ℝ := (∑ v : G.V, f v) / (G.order : ℝ) + +/-- The mean-zero part of a function. -/ +noncomputable def center (f : G.V → ℝ) : G.V → ℝ := fun v => f v - G.mean f + +theorem eq_mean_add_center (f : G.V → ℝ) : f = fun v => G.mean f + G.center f v := by + funext v; simp [center] + +theorem sum_center (hn : 0 < G.order) (f : G.V → ℝ) : ∑ v : G.V, G.center f v = 0 := by + simp only [center, Finset.sum_sub_distrib, Finset.sum_const, Finset.card_univ, + nsmul_eq_mul, mean, card_eq_order] + field_simp + ring + +theorem sum_sq_center (hn : 0 < G.order) (f : G.V → ℝ) : + ∑ v : G.V, (G.center f v) ^ 2 + = (∑ v : G.V, (f v) ^ 2) - (∑ v : G.V, f v) ^ 2 / (G.order : ℝ) := by + have expand : ∀ v : G.V, (G.center f v) ^ 2 + = (f v) ^ 2 - 2 * G.mean f * f v + (G.mean f) ^ 2 := by + intro v; simp only [center]; ring + calc ∑ v : G.V, (G.center f v) ^ 2 + = ∑ v : G.V, ((f v) ^ 2 - 2 * G.mean f * f v + (G.mean f) ^ 2) := + Finset.sum_congr rfl fun v _ => expand v + _ = (∑ v : G.V, (f v) ^ 2) - 2 * G.mean f * (∑ v : G.V, f v) + + (G.order : ℝ) * (G.mean f) ^ 2 := by + rw [Finset.sum_add_distrib, Finset.sum_sub_distrib, ← Finset.mul_sum] + simp + _ = (∑ v : G.V, (f v) ^ 2) - (∑ v : G.V, f v) ^ 2 / (G.order : ℝ) := by + simp only [mean] + field_simp + ring + +theorem sum_sq_center_nonneg (f : G.V → ℝ) : 0 ≤ ∑ v : G.V, (G.center f v) ^ 2 := + Finset.sum_nonneg fun _ _ => sq_nonneg _ + +/-! ### The mixing lemma -/ + +/-- The correlation between `f` at the start of a `t`-step walk and `g` at its +end splits into the product of averages plus the correlation of the mean-zero +parts. -/ +theorem inner_stepIter_eq (hn : 0 < G.order) (t : ℕ) (f g : G.V → ℝ) : + ∑ v : G.V, f v * G.stepIter t g v + = (∑ v : G.V, f v) * (∑ v : G.V, g v) / (G.order : ℝ) + + ∑ v : G.V, G.center f v * G.stepIter t (G.center g) v := by + have hsumS : ∑ v : G.V, G.stepIter t (G.center g) v = 0 := by + rw [G.sum_stepIter, G.sum_center hn] + have hsumF : ∑ v : G.V, G.center f v = 0 := G.sum_center hn f + have hg : G.stepIter t g = fun v => G.mean g + G.stepIter t (G.center g) v := by + conv_lhs => rw [G.eq_mean_add_center g] + rw [G.stepIter_add, G.stepIter_const] + have hf : ∀ v : G.V, f v = G.mean f + G.center f v := fun v => by + simp [center] + calc ∑ v : G.V, f v * G.stepIter t g v + = ∑ v : G.V, (G.mean f + G.center f v) + * (G.mean g + G.stepIter t (G.center g) v) := by + rw [hg] + exact Finset.sum_congr rfl fun v _ => by rw [hf v] + _ = ∑ v : G.V, (G.mean f * G.mean g + + (G.mean f * G.stepIter t (G.center g) v + + (G.mean g * G.center f v + + G.center f v * G.stepIter t (G.center g) v))) := + Finset.sum_congr rfl fun v _ => by ring + _ = (G.order : ℝ) * (G.mean f * G.mean g) + + (G.mean f * (∑ v : G.V, G.stepIter t (G.center g) v) + + (G.mean g * (∑ v : G.V, G.center f v) + + ∑ v : G.V, G.center f v * G.stepIter t (G.center g) v)) := by + simp only [Finset.sum_add_distrib, ← Finset.mul_sum, Finset.sum_const, + Finset.card_univ, nsmul_eq_mul, card_eq_order] + ring + _ = (G.order : ℝ) * (G.mean f * G.mean g) + + ∑ v : G.V, G.center f v * G.stepIter t (G.center g) v := by + rw [hsumS, hsumF]; ring + _ = (∑ v : G.V, f v) * (∑ v : G.V, g v) / (G.order : ℝ) + + ∑ v : G.V, G.center f v * G.stepIter t (G.center g) v := by + simp only [mean] + field_simp + +/-- **The expander mixing lemma for `t`-step walks.** -/ +theorem mixing_sq {lam : ℝ} (h : G.SpectralBound lam) (hn : 0 < G.order) (t : ℕ) + (f g : G.V → ℝ) : + (∑ v : G.V, f v * G.stepIter t g v + - (∑ v : G.V, f v) * (∑ v : G.V, g v) / (G.order : ℝ)) ^ 2 + ≤ lam ^ (2 * t) * ((∑ v : G.V, (f v) ^ 2) - (∑ v : G.V, f v) ^ 2 / (G.order : ℝ)) + * ((∑ v : G.V, (g v) ^ 2) - (∑ v : G.V, g v) ^ 2 / (G.order : ℝ)) := by + have hcorr : ∑ v : G.V, f v * G.stepIter t g v + - (∑ v : G.V, f v) * (∑ v : G.V, g v) / (G.order : ℝ) + = ∑ v : G.V, G.center f v * G.stepIter t (G.center g) v := by + rw [G.inner_stepIter_eq hn t f g]; ring + rw [hcorr, ← G.sum_sq_center hn f, ← G.sum_sq_center hn g] + have hcs : (∑ v : G.V, G.center f v * G.stepIter t (G.center g) v) ^ 2 + ≤ (∑ v : G.V, (G.center f v) ^ 2) + * ∑ v : G.V, (G.stepIter t (G.center g) v) ^ 2 := + Finset.sum_mul_sq_le_sq_mul_sq _ _ _ + have hspec : ∑ v : G.V, (G.stepIter t (G.center g) v) ^ 2 + ≤ lam ^ (2 * t) * ∑ v : G.V, (G.center g v) ^ 2 := + G.sum_sq_stepIter_le h t (G.center g) (G.sum_center hn g) + calc (∑ v : G.V, G.center f v * G.stepIter t (G.center g) v) ^ 2 + ≤ (∑ v : G.V, (G.center f v) ^ 2) + * ∑ v : G.V, (G.stepIter t (G.center g) v) ^ 2 := hcs + _ ≤ (∑ v : G.V, (G.center f v) ^ 2) + * (lam ^ (2 * t) * ∑ v : G.V, (G.center g v) ^ 2) := by + exact mul_le_mul_of_nonneg_left hspec (G.sum_sq_center_nonneg f) + _ = lam ^ (2 * t) * (∑ v : G.V, (G.center f v) ^ 2) + * ∑ v : G.V, (G.center g v) ^ 2 := by ring + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/NatEncode.lean b/Complexitylib/Classes/PCP/Internal/NatEncode.lean new file mode 100644 index 00000000..e710d337 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/NatEncode.lean @@ -0,0 +1,201 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.PCP.Internal.StripTrailing +public import Complexitylib.Classes.PCP.Internal.PosScan + +/-! +# Writing out an encoded bit list + +`DataEncode` serializes a list of booleans as a substitution cipher inside one +pair of brackets: `false` becomes `01` and `true` becomes `0011`. Producing that +is a fold over the list, which `recFoldClamp` runs in polynomial time. + +Since a natural number is encoded *as* its `Nat.bits`, this is also the last +step of encoding a number: count the value out in binary, drop the trailing +zeros, and run the cipher. + +## Main definitions + +- `Complexity.boolBits` — the two-symbol cipher + +## Main results + +- `Complexity.bitstringEncode_list` — the cipher describes the encoding +- `Complexity.flatBitsFn_mem_FP` — running it is polynomial time +- `Complexity.natEncodeFn_eq` — a number's encoding, from its value in unary +-/ + +@[expose] public section + +namespace Complexity + +/-- The serialization of a single boolean. -/ +def boolBits (b : Bool) : List Bool := if b then [false, false, true, true] else [false, true] + +theorem boolBits_eq (b : Bool) : boolBits b = (DataEncode.encode b).toBits := by + cases b + · show [false, true] = (Data.l []).toBits + rw [Data.toBits_l] + simp + · show [false, false, true, true] = (Data.l [Data.l []]).toBits + rw [Data.toBits_l] + simp only [List.map_cons, List.map_nil, List.flatten_cons, List.flatten_nil] + rw [show (Data.l ([] : List Data)).toBits = [false, true] from by + rw [Data.toBits_l]; simp] + simp + +@[simp] theorem length_boolBits (b : Bool) : (boolBits b).length ≤ 4 := by + cases b <;> simp [boolBits] + +/-- **The cipher describes the encoding.** -/ +theorem bitstringEncode_list (l : List Bool) : + DataEncode.bitstringEncode l = false :: (l.flatMap boolBits) ++ [true] := by + rw [DataEncode.bitstringEncode_def, + show DataEncode.encode l = Data.l (l.map DataEncode.encode) from rfl, Data.toBits_l, + List.map_map] + congr 2 + rw [List.flatMap_def] + congr 1 + refine List.map_congr_left fun b _ => ?_ + rw [Function.comp_apply, boolBits_eq] + +/-! ### Running the cipher -/ + +/-- The fold step on a zero. -/ +def blitZero (z : List Bool) : List Bool := + [false, true] ++ pairSnd (pairFst z) + +/-- The fold step on a one. -/ +def blitOne (z : List Bool) : List Bool := + [false, false, true, true] ++ pairSnd (pairFst z) + +theorem blitZero_mem_FP : blitZero ∈ FP := by + have h : (fun z : List Bool => pairSnd (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + exact Cobham.appendFn_mem_FP (constFn_mem_FP [false, true]) h + +theorem blitOne_mem_FP : blitOne ∈ FP := by + have h : (fun z : List Bool => pairSnd (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + exact Cobham.appendFn_mem_FP (constFn_mem_FP [false, false, true, true]) h + +theorem length_flatMap_boolBits (l : List Bool) : + (l.flatMap boolBits).length ≤ 4 * l.length := by + induction l with + | nil => simp + | cons b t ih => + rw [List.flatMap_cons, List.length_append, List.length_cons] + have := length_boolBits b + omega + +theorem recFoldClamp_flatBits (bound : ℕ) (W : List Bool) : + ∀ l : List Bool, 4 * l.length ≤ bound → + Cobham.recFoldClamp blitZero blitOne bound [] W l = l.flatMap boolBits := by + intro l + induction l with + | nil => + intro _ + rw [Cobham.recFoldClamp] + simp + | cons b t ih => + intro hb + have hb' : 4 * t.length ≤ bound := by + simp only [List.length_cons] at hb + omega + rw [Cobham.recFoldClamp, ih hb'] + have hstate : pairSnd (pairFst + (pair (pair W (t.flatMap boolBits)) t)) = t.flatMap boolBits := by + rw [pairFst_pair, pairSnd_pair] + have hlt := length_flatMap_boolBits t + cases b + · show (blitZero _).take bound = _ + rw [blitZero, hstate, List.flatMap_cons, + show boolBits false = [false, true] from rfl] + refine List.take_of_length_le ?_ + rw [List.length_append] + simp only [List.length_cons, List.length_nil, List.length_cons] at hb ⊢ + omega + · show (blitOne _).take bound = _ + rw [blitOne, hstate, List.flatMap_cons, + show boolBits true = [false, false, true, true] from rfl] + refine List.take_of_length_le ?_ + rw [List.length_append] + simp only [List.length_cons, List.length_nil, List.length_cons] at hb ⊢ + omega + +/-- The cipher applied to `pairSnd z`. -/ +def flatBitsFn (z : List Bool) : List Bool := + Cobham.recFoldClamp blitZero blitOne (4 * z.length) [] (pairFst z) + (pairSnd z) + +theorem flatBitsFn_mem_FP : flatBitsFn ∈ FP := by + have := Cobham.recFoldClamp_mem_FP blitZero_mem_FP blitOne_mem_FP + (constFn_mem_FP []) (Polynomial.C 4 * Polynomial.X) + refine mem_FP_of_eq this fun z => ?_ + rw [flatBitsFn] + simp + +theorem flatBitsFn_eq (z : List Bool) : + flatBitsFn z = (pairSnd z).flatMap boolBits := by + refine recFoldClamp_flatBits _ _ _ ?_ + have := pairSnd_length_le z + omega + +/-- **The encoding of a bit list, in polynomial time.** -/ +def encodeListFn (z : List Bool) : List Bool := false :: flatBitsFn z ++ [true] + +theorem encodeListFn_mem_FP : encodeListFn ∈ FP := by + have hcons := mem_FP_comp flatBitsFn_mem_FP (Cobham.cons_mem_FP false) + have := Cobham.appendFn_mem_FP hcons (constFn_mem_FP [true]) + refine mem_FP_of_eq this fun z => ?_ + rw [encodeListFn] + simp + +theorem encodeListFn_eq (z : List Bool) : + encodeListFn z = DataEncode.bitstringEncode (pairSnd z) := by + rw [encodeListFn, flatBitsFn_eq, bitstringEncode_list] + +/-! ### A number's own encoding -/ + +/-- **The encoding of a natural number**, from a width and a value both given in +unary. -/ +noncomputable def natEncodeFn (z : List Bool) : List Bool := + encodeListFn (pair [] (stripFn (pair [] + (coinStr (pairFst z).length (pairSnd z).length)))) + +theorem natEncodeFn_mem_FP : natEncodeFn ∈ FP := by + have hw : (fun z : List Bool => + List.replicate (pairFst z).length true) ∈ FP := by + have := mem_FP_comp Cobham.fstBlock_mem_FP unaryLength_mem_FP + simpa using this + have hv : (fun z : List Bool => + List.replicate (pairSnd z).length true) ∈ FP := by + have := mem_FP_comp Cobham.sndBlock_mem_FP unaryLength_mem_FP + simpa using this + have hcoin := coinStr_mem_FP hw hv + have h1 : (fun z => pair [] (coinStr (pairFst z).length + (pairSnd z).length)) ∈ FP := + Cobham.pairFn_mem_FP (constFn_mem_FP []) hcoin + have h2 := mem_FP_comp h1 stripFn_mem_FP + have h3 : (fun z => pair [] (stripFn (pair [] + (coinStr (pairFst z).length (pairSnd z).length)))) ∈ FP := by + refine Cobham.pairFn_mem_FP (constFn_mem_FP []) ?_ + simpa using h2 + have := mem_FP_comp h3 encodeListFn_mem_FP + simpa using this + +/-- **It really is the number's encoding**, whenever the width holds the +value. -/ +theorem natEncodeFn_eq {z : List Bool} + (h : (pairSnd z).length < 2 ^ (pairFst z).length) : + natEncodeFn z = DataEncode.bitstringEncode ((pairSnd z).length) := by + rw [natEncodeFn, encodeListFn_eq, pairSnd_pair, stripFn_eq, pairSnd_pair, + coinStr_eq h, stripTrailing_eq_bits, binValLE_bitsOfLenLE _ _ h] + rfl + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/NumEnc.lean b/Complexitylib/Classes/PCP/Internal/NumEnc.lean new file mode 100644 index 00000000..43dd87d5 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/NumEnc.lean @@ -0,0 +1,339 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Mathlib.Data.Fintype.Card +public import Mathlib.Data.Fintype.Pi +public import Mathlib.Data.Fintype.Sum +public import Mathlib.Tactic.NormNum +public import Mathlib.Tactic.Ring + +/-! +# Numbering a structured index + +The index types of Dinur's construction are built from a few formers: a pair of +indices, a choice between two, an optional one, a tuple. `Fintype` numbers such +a type too, but by an enumeration no algorithm can follow. This module numbers +them explicitly instead: a pair is numbered by mixed radix, a choice by +offsetting the second summand, a tuple by its digits. + +The decoding is partial — a number out of range names nothing — which is what +makes the numbering compose without needing any type to be inhabited. + +## Main definitions + +- `Complexity.NumEnc` — an explicit numbering of a finite type + +## Main results + +- the instances for `Fin`, `Bool`, `Unit`, products, sums, options and tuples +-/ + +@[expose] public section + +namespace Complexity + +/-- An explicit numbering of a finite type: every value has a number below +`card`, and every number below `card` names a value. -/ +class NumEnc (α : Type) where + /-- How many values there are. -/ + card : ℕ + /-- The number of a value. -/ + enc : α → ℕ + /-- The value a number names, if any. -/ + dec : ℕ → Option α + /-- Numbers are below the count. -/ + enc_lt : ∀ a, enc a < card + /-- Decoding a number back gives the value. -/ + dec_enc : ∀ a, dec (enc a) = some a + /-- And a number in range is the number of what it names. -/ + enc_dec : ∀ i a, dec i = some a → enc a = i + /-- Every number below the count names something. -/ + dec_isSome : ∀ i, i < card → (dec i).isSome + +namespace NumEnc + +open NumEnc (card enc dec) + +variable {α β : Type} + +theorem enc_injective [NumEnc α] : Function.Injective (enc : α → ℕ) := by + intro a b h + have ha := dec_enc a + rw [h, dec_enc b] at ha + exact (Option.some_injective _ ha).symm + +/-- The value a number in range names. -/ +def get [NumEnc α] {i : ℕ} (h : i < card α) : α := (dec i).get (dec_isSome i h) + +@[simp] theorem dec_get [NumEnc α] {i : ℕ} (h : i < card α) : dec (α := α) i = some (get h) := + (Option.some_get _).symm + +@[simp] theorem enc_get [NumEnc α] {i : ℕ} (h : i < card α) : enc (get h) = i := + enc_dec i _ (dec_get h) + +@[simp] theorem get_enc [NumEnc α] (a : α) : get (enc_lt a) = a := by + have h := dec_get (enc_lt a) + rw [dec_enc a] at h + exact (Option.some_injective _ h).symm + +/-- **The count is the number of values.** -/ +theorem card_eq_fintype_card (α : Type) [Fintype α] [NumEnc α] : + card α = Fintype.card α := by + have hbij : Function.Bijective (fun i : Fin (card α) => get i.isLt) := by + constructor + · intro i j h + have hi : enc (get i.isLt) = i.val := enc_get i.isLt + have hj : enc (get j.isLt) = j.val := enc_get j.isLt + have hij : enc (get i.isLt) = enc (get j.isLt) := congrArg enc h + rw [hi, hj] at hij + exact Fin.ext hij + · intro a + exact ⟨⟨enc a, enc_lt a⟩, get_enc a⟩ + have := Fintype.card_of_bijective hbij + rw [Fintype.card_fin] at this + exact this + +/-- The numbering, as an equivalence with an initial segment. -/ +def equivFin (α : Type) [NumEnc α] : α ≃ Fin (card α) where + toFun a := ⟨enc a, enc_lt a⟩ + invFun i := get i.isLt + left_inv a := by simp + right_inv i := Fin.ext (enc_get i.isLt) + +/-- The numbering, as an equivalence with `Fin` of the type's own cardinality — +a drop-in replacement for `Fintype.equivFin` that an algorithm can follow. -/ +noncomputable def equivFinCard (α : Type) [Fintype α] [NumEnc α] : α ≃ Fin (Fintype.card α) := + (equivFin α).trans (finCongr (card_eq_fintype_card α)) + +@[simp] theorem val_equivFinCard (α : Type) [Fintype α] [NumEnc α] (a : α) : + (equivFinCard α a).val = enc a := rfl + +/-! ### The formers -/ + +instance instFin (n : ℕ) : NumEnc (Fin n) where + card := n + enc i := i.val + dec i := if h : i < n then some ⟨i, h⟩ else none + enc_lt i := i.isLt + dec_enc i := by rw [dif_pos i.isLt] + enc_dec i a h := by + by_cases hi : i < n + · rw [dif_pos hi] at h + exact congrArg Fin.val (Option.some_injective _ h).symm + · rw [dif_neg hi] at h + exact absurd h (by simp) + dec_isSome i hi := by rw [dif_pos hi]; rfl + +instance instBool : NumEnc Bool where + card := 2 + enc b := if b then 0 else 1 + dec i := if i = 0 then some true else if i = 1 then some false else none + enc_lt b := by cases b <;> norm_num + dec_enc b := by cases b <;> norm_num + enc_dec i a h := by + by_cases h0 : i = 0 + · subst h0 + simp at h + subst h + norm_num + · by_cases h1 : i = 1 + · subst h1 + simp at h + subst h + norm_num + · rw [if_neg h0, if_neg h1] at h + exact absurd h (by simp) + dec_isSome i hi := by + by_cases h0 : i = 0 + · subst h0; rfl + · have h1 : i = 1 := by omega + subst h1; rfl + +instance instUnit : NumEnc Unit where + card := 1 + enc _ := 0 + dec i := if i = 0 then some () else none + enc_lt _ := by norm_num + dec_enc _ := by norm_num + enc_dec i a h := by + by_cases h0 : i = 0 + · subst h0; rfl + · rw [if_neg h0] at h + exact absurd h (by simp) + dec_isSome i hi := by + have h0 : i = 0 := by omega + subst h0 + rfl + +theorem prod_lt [NumEnc α] [NumEnc β] (a : α) (b : β) : + enc a * card β + enc b < card α * card β := by + have h1 := enc_lt a + have h2 := enc_lt b + calc enc a * card β + enc b < enc a * card β + card β := by omega + _ = (enc a + 1) * card β := by ring + _ ≤ card α * card β := Nat.mul_le_mul_right _ h1 + +theorem prod_div [NumEnc α] [NumEnc β] (a : α) (b : β) : + (enc a * card β + enc b) / card β = enc a := by + have h2 := enc_lt b + have hb : 0 < card β := by omega + rw [show enc a * card β + enc b = enc b + card β * enc a by ring, + Nat.add_mul_div_left _ _ hb, Nat.div_eq_of_lt h2, Nat.zero_add] + +theorem prod_mod [NumEnc α] [NumEnc β] (a : α) (b : β) : + (enc a * card β + enc b) % card β = enc b := by + have h2 := enc_lt b + rw [show enc a * card β + enc b = enc b + card β * enc a by ring, + Nat.add_mul_mod_self_left, Nat.mod_eq_of_lt h2] + +instance instProd [NumEnc α] [NumEnc β] : NumEnc (α × β) where + card := card α * card β + enc p := enc p.1 * card β + enc p.2 + dec i := + if i < card α * card β then + (dec (i / card β)).bind fun a => (dec (i % card β)).map fun b => (a, b) + else none + enc_lt p := prod_lt p.1 p.2 + dec_enc p := by + rw [if_pos (prod_lt p.1 p.2), prod_div, prod_mod, dec_enc, dec_enc] + rfl + enc_dec i p h := by + by_cases hlt : i < card α * card β + · rw [if_pos hlt] at h + rw [Option.bind_eq_some_iff] at h + obtain ⟨a, ha, hb⟩ := h + rw [Option.map_eq_some_iff] at hb + obtain ⟨b, hbb, hp⟩ := hb + have hea := enc_dec _ a ha + have heb := enc_dec _ b hbb + show enc p.1 * card β + enc p.2 = i + rw [← hp] + show enc a * card β + enc b = i + rw [hea, heb, Nat.mul_comm] + exact Nat.div_add_mod i (card β) + · rw [if_neg hlt] at h + exact absurd h (by simp) + + dec_isSome i hi := by + have hb : 0 < card β := by + rcases Nat.eq_zero_or_pos (card β) with h | h + · rw [h, Nat.mul_zero] at hi; omega + · exact h + have hia : i / card β < card α := (Nat.div_lt_iff_lt_mul hb).mpr hi + have him : i % card β < card β := Nat.mod_lt _ hb + obtain ⟨a, ha⟩ := Option.isSome_iff_exists.mp (dec_isSome _ hia) + obtain ⟨b, hbb⟩ := Option.isSome_iff_exists.mp (dec_isSome _ him) + rw [if_pos hi, ha, hbb] + rfl + +instance instSum [NumEnc α] [NumEnc β] : NumEnc (α ⊕ β) where + card := card α + card β + enc := Sum.elim enc fun b => card α + enc b + dec i := if i < card α then (dec i).map Sum.inl else (dec (i - card α)).map Sum.inr + enc_lt x := by + cases x with + | inl a => have := enc_lt a; simpa using by omega + | inr b => have := enc_lt b; simpa using by omega + dec_enc x := by + cases x with + | inl a => + have ha := enc_lt a + show (if enc a < card α then _ else _) = _ + simp only [Sum.elim_inl] + rw [if_pos ha, dec_enc] + rfl + | inr b => + have hb : ¬ card α + enc b < card α := by omega + show (if card α + enc b < card α then _ else _) = _ + simp only [Sum.elim_inr] + rw [if_neg hb, Nat.add_sub_cancel_left, dec_enc] + rfl + enc_dec i x h := by + by_cases hi : i < card α + · rw [if_pos hi, Option.map_eq_some_iff] at h + obtain ⟨a, ha, hx⟩ := h + have := enc_dec _ a ha + rw [← hx] + show enc a = i + exact this + · rw [if_neg hi, Option.map_eq_some_iff] at h + obtain ⟨b, hb, hx⟩ := h + have := enc_dec _ b hb + rw [← hx] + show card α + enc b = i + omega + + dec_isSome i hi := by + by_cases h : i < card α + · obtain ⟨a, ha⟩ := Option.isSome_iff_exists.mp (dec_isSome (α := α) _ h) + rw [if_pos h, ha] + rfl + · have hb : i - card α < card β := by omega + obtain ⟨b, hbb⟩ := Option.isSome_iff_exists.mp (dec_isSome (α := β) _ hb) + rw [if_neg h, hbb] + rfl + +instance instOption [NumEnc α] : NumEnc (Option α) where + card := card α + 1 + enc o := match o with | none => 0 | some a => 1 + enc a + dec i := if i = 0 then some none else (dec (i - 1)).map some + enc_lt o := by + cases o with + | none => show 0 < card α + 1; omega + | some a => have := enc_lt a; show 1 + enc a < _; omega + dec_enc o := by + cases o with + | none => show (if (0 : ℕ) = 0 then _ else _) = _; rw [if_pos rfl] + | some a => + show (if 1 + enc a = 0 then _ else _) = _ + rw [if_neg (by omega), show 1 + enc a - 1 = enc a by omega, dec_enc] + rfl + enc_dec i o h := by + by_cases h0 : i = 0 + · rw [if_pos h0] at h + rw [← Option.some_injective _ h] + show 0 = i + omega + · rw [if_neg h0, Option.map_eq_some_iff] at h + obtain ⟨a, ha, ho⟩ := h + have := enc_dec _ a ha + rw [← ho] + show 1 + enc a = i + omega + dec_isSome i hi := by + by_cases h0 : i = 0 + · rw [if_pos h0] + rfl + · have hb : i - 1 < card α := by omega + obtain ⟨a, ha⟩ := Option.isSome_iff_exists.mp (dec_isSome (α := α) _ hb) + rw [if_neg h0, ha] + rfl + +/-- Any finite type is numbered by its own enumeration. For a type whose size +is a constant, that is all an algorithm needs: the numbering is a lookup on a +bounded key. -/ +@[reducible] noncomputable def ofFintype (α : Type) [Fintype α] : NumEnc α where + card := Fintype.card α + enc a := (Fintype.equivFin α a).val + dec i := if h : i < Fintype.card α then some ((Fintype.equivFin α).symm ⟨i, h⟩) else none + enc_lt a := (Fintype.equivFin α a).isLt + dec_enc a := by + rw [dif_pos (Fintype.equivFin α a).isLt] + simp + enc_dec i a h := by + by_cases hi : i < Fintype.card α + · rw [dif_pos hi] at h + rw [← Option.some_injective _ h] + simp + · rw [dif_neg hi] at h + exact absurd h (by simp) + dec_isSome i hi := by + rw [dif_pos hi] + rfl + +end NumEnc + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/NumEncPi.lean b/Complexitylib/Classes/PCP/Internal/NumEncPi.lean new file mode 100644 index 00000000..20a1a5c2 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/NumEncPi.lean @@ -0,0 +1,176 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.NumEnc +public import Mathlib.Algebra.BigOperators.Fin + +/-! +# Numbering a tuple + +A walk is a tuple of darts, and a coin sequence is a tuple of coins. This module +numbers such a tuple the way a numeral works: the `j`-th entry contributes its +own number times the base to the `j`-th power. Reading an entry back is dividing +by that power and taking the remainder, which is what an algorithm does. + +## Main results + +- `Complexity.NumEnc.instPi` — the `NumEnc` instance for `Fin n → α` +- `Complexity.NumEnc.digit_sum`, `Complexity.NumEnc.sum_digits` — reading a + digit, and reassembling a number from its digits +-/ + +@[expose] public section + +namespace Complexity + +namespace NumEnc + +open NumEnc (card enc dec) + +variable {α : Type} + +/-! ### Digits -/ + +theorem sum_lt_pow {c n : ℕ} (g : ℕ → ℕ) (hg : ∀ i < n, g i < c) : + ∑ i ∈ Finset.range n, g i * c ^ i < c ^ n := by + induction n with + | zero => simp + | succ n ih => + rw [Finset.sum_range_succ, pow_succ] + have h1 : ∑ i ∈ Finset.range n, g i * c ^ i < c ^ n := ih fun i hi => hg i (by omega) + have h2 : g n < c := hg n (by omega) + calc (∑ i ∈ Finset.range n, g i * c ^ i) + g n * c ^ n + < c ^ n + g n * c ^ n := by omega + _ = (g n + 1) * c ^ n := by ring + _ ≤ c * c ^ n := Nat.mul_le_mul_right _ (by omega) + _ = c ^ n * c := by ring + +theorem digit_sum {c : ℕ} (hc : 0 < c) (g : ℕ → ℕ) : + ∀ {n j : ℕ}, j < n → (∀ i < n, g i < c) → + (∑ i ∈ Finset.range n, g i * c ^ i) / c ^ j % c = g j := by + intro n + induction n with + | zero => intro j hj; omega + | succ n ih => + intro j hj hg + rcases Nat.lt_or_ge j n with hjn | hjn + · have hstep : (∑ i ∈ Finset.range (n + 1), g i * c ^ i) / c ^ j % c + = (∑ i ∈ Finset.range n, g i * c ^ i) / c ^ j % c := by + rw [Finset.sum_range_succ] + have hpow : c ^ n = c ^ j * c ^ (n - j) := by + rw [← pow_add] + congr 1 + omega + have hdvd : c ∣ c ^ (n - j) := dvd_pow_self c (by omega) + obtain ⟨t, ht⟩ := hdvd + rw [hpow, ht] + rw [show g n * (c ^ j * (c * t)) = c ^ j * (g n * (c * t)) by ring, + Nat.add_mul_div_left _ _ (Nat.pow_pos hc), + show g n * (c * t) = (g n * t) * c by ring, Nat.add_mul_mod_self_right] + rw [hstep] + exact ih hjn fun i hi => hg i (by omega) + · have hjeq : j = n := by omega + subst hjeq + rw [Finset.sum_range_succ] + have hlt : ∑ i ∈ Finset.range j, g i * c ^ i < c ^ j := + sum_lt_pow g fun i hi => hg i (by omega) + rw [show g j * c ^ j = c ^ j * g j by ring, + Nat.add_mul_div_left _ _ (Nat.pow_pos hc), Nat.div_eq_of_lt hlt, + Nat.zero_add, Nat.mod_eq_of_lt (hg j (by omega))] + +theorem sum_digits {c : ℕ} : + ∀ {n i : ℕ}, i < c ^ n → ∑ j ∈ Finset.range n, (i / c ^ j % c) * c ^ j = i := by + intro n + induction n with + | zero => + intro i hi + simp only [pow_zero] at hi + simp + omega + | succ n ih => + intro i hi + rw [Finset.sum_range_succ'] + have h1 : ∀ j, i / c ^ (j + 1) % c = (i / c) / c ^ j % c := by + intro j + rw [pow_succ, Nat.div_div_eq_div_mul, Nat.mul_comm] + have h2 : i / c < c ^ n := by + refine Nat.div_lt_of_lt_mul ?_ + rw [Nat.mul_comm, ← pow_succ] + exact hi + have h3 : ∑ j ∈ Finset.range n, (i / c ^ (j + 1) % c) * c ^ (j + 1) + = (∑ j ∈ Finset.range n, ((i / c) / c ^ j % c) * c ^ j) * c := by + rw [Finset.sum_mul] + refine Finset.sum_congr rfl fun j _ => ?_ + rw [h1 j, pow_succ] + ring + rw [h3, ih h2] + simp only [pow_zero, Nat.mul_one, Nat.div_one] + rw [Nat.mul_comm] + exact Nat.div_add_mod i c + +/-! ### The instance -/ + +theorem get_eq [NumEnc α] {i : ℕ} (hi : i < card α) {a : α} (h : i = enc a) : + get hi = a := by + have h1 : enc (get hi) = i := enc_get hi + exact enc_injective (h1.trans h) + +/-- The number of the `i`-th entry of a tuple, or zero past its end. -/ +def encAt {n : ℕ} [NumEnc α] (f : Fin n → α) (i : ℕ) : ℕ := + if h : i < n then enc (f ⟨i, h⟩) else 0 + +theorem encAt_lt {n : ℕ} [NumEnc α] (f : Fin n → α) {i : ℕ} (hi : i < n) : + encAt f i < card α := by + rw [encAt, dif_pos hi] + exact enc_lt _ + +/-- **A tuple is numbered like a numeral.** -/ +instance instPi (n : ℕ) [NumEnc α] : NumEnc (Fin n → α) where + card := card α ^ n + enc f := ∑ i ∈ Finset.range n, encAt f i * card α ^ i + dec i := + if h : i < card α ^ n then + some fun j : Fin n => + get (show i / card α ^ j.val % card α < card α from by + have hpos : 0 < card α ^ n := Nat.lt_of_le_of_lt (Nat.zero_le _) h + have hc : 0 < card α := by + by_contra hcon + have hz : card α = 0 := by omega + have hn : 0 < n := Nat.lt_of_le_of_lt (Nat.zero_le _) j.isLt + rw [hz, zero_pow (by omega)] at hpos + omega + exact Nat.mod_lt _ hc) + else none + enc_lt f := sum_lt_pow _ fun i hi => encAt_lt f hi + dec_enc f := by + have hlt : (∑ i ∈ Finset.range n, encAt f i * card α ^ i) < card α ^ n := + sum_lt_pow _ fun i hi => encAt_lt f hi + rw [dif_pos hlt] + congr 1 + funext j + have hc : 0 < card α := Nat.lt_of_le_of_lt (Nat.zero_le _) (enc_lt (f j)) + have hdig := digit_sum (c := card α) hc (encAt f) j.isLt + (fun i hi => encAt_lt f hi) + have hval : encAt f j.val = enc (f j) := by + rw [encAt, dif_pos j.isLt] + exact get_eq _ (hdig.trans hval) + enc_dec i f h := by + by_cases hi : i < card α ^ n + · rw [dif_pos hi] at h + have hf := Option.some_injective _ h + subst hf + show ∑ j ∈ Finset.range n, encAt _ j * card α ^ j = i + refine Eq.trans (Finset.sum_congr rfl fun j hj => ?_) (sum_digits hi) + rw [encAt, dif_pos (Finset.mem_range.mp hj), enc_get] + · rw [dif_neg hi] at h + exact absurd h (by simp) + dec_isSome i hi := by + rw [dif_pos hi] + rfl + +end NumEnc + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/PCPtoSAT.lean b/Complexitylib/Classes/PCP/Internal/PCPtoSAT.lean new file mode 100644 index 00000000..c98e4bcb --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/PCPtoSAT.lean @@ -0,0 +1,145 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.SubsetNP +public import Complexitylib.SAT.Semantics +public import Complexitylib.SAT.Language +public import Complexitylib.SAT.Verifier + +/-! +# A PCP verifier as a CNF formula + +`SubsetNP` reduces "some proof is accepted on every coin string" to "some +bitstring is a witness": a table, laid out one block of `Q` answers per coin +string, that is consistent and accepted everywhere. Both conditions are +predicates on individual bits of that bitstring, so both are CNF clauses. + +That is what this module builds. The formula's variables *are* the positions of +the witness — a SAT assignment and a witness are the same object, since both +read out of range as `false` — so the encoding needs no translation of models. + +* Consistency contributes, for each pair of query slots that read the same proof + position, the two clauses saying their variables agree. +* Acceptance contributes, for each coin string and each answer vector the + verdict rejects, the clause blocking that vector. + +With `r` coins and `q` queries the formula has `2^r q` variables and +`O(4^r q^2 + 2^r 2^q)` clauses — polynomial when `r` is logarithmic and `q` +constant. + +## Main definitions + +- `Complexity.allVecs` — the bit vectors of a given length +- `Complexity.PCPVerifier.varIdx` — the variable holding one answer +- `Complexity.PCPVerifier.toCNF` — the formula + +## Main results + +- `Complexity.mem_allVecs_iff` — `allVecs n` is exactly the vectors of length `n` +-/ + +@[expose] public section + +namespace Complexity + +open SAT + +/-! ### Enumerating bit vectors -/ + +/-- Every bit vector of a given length. -/ +def allVecs : ℕ → List (List Bool) + | 0 => [[]] + | n + 1 => (allVecs n).flatMap fun v => [false :: v, true :: v] + +theorem mem_allVecs_iff : ∀ (n : ℕ) (b : List Bool), b ∈ allVecs n ↔ b.length = n := by + intro n + induction n with + | zero => + intro b + constructor + · intro hb + simp only [allVecs, List.mem_singleton] at hb + rw [hb] + rfl + · intro hb + have : b = [] := List.length_eq_zero_iff.1 hb + rw [this] + simp [allVecs] + | succ m ih => + intro b + constructor + · intro hb + simp only [allVecs, List.mem_flatMap] at hb + obtain ⟨v, hv, hbv⟩ := hb + have hlen : v.length = m := (ih v).1 hv + simp only [List.mem_cons] at hbv + rcases hbv with h | h | h + · rw [h, List.length_cons, hlen] + · rw [h, List.length_cons, hlen] + · exact absurd h (by simp) + · intro hb + match b with + | [] => exact absurd hb (by simp) + | c :: v => + have hlen : v.length = m := by + rw [List.length_cons] at hb + omega + simp only [allVecs, List.mem_flatMap] + refine ⟨v, (ih v).2 hlen, ?_⟩ + cases c <;> simp + +namespace PCPVerifier + +variable (V : PCPVerifier) + +/-! ### Variables -/ + +/-- The variable holding the answer to query `i` on coin string `ρ`. The blocks +sit a stride `Q` apart, exactly as `SubsetNP.tableOf` reads them. -/ +def varIdx (t Q : ℕ) (ρ : Fin t → Bool) (i : ℕ) : ℕ := coinIndex ρ * Q + i + +/-! ### The clauses -/ + +/-- The coin strings, listed by index. Computable, unlike an enumeration drawn +from `Finset.univ`, because the reduction has to be carried out by a machine. -/ +def coinList (t : ℕ) : List (Fin t → Bool) := + (List.finRange (2 ^ t)).map coinOfIndex + +/-- Two query slots reading the same proof position must get the same answer. -/ +def consClauses (t Q : ℕ) (x : List Bool) : List Clause := + (coinList t).flatMap fun ρ => + (coinList t).flatMap fun ρ' => + (List.range (V.positions x (BitString.toList ρ)).length).flatMap fun i => + (List.range (V.positions x (BitString.toList ρ')).length).flatMap fun i' => + if (V.positions x (BitString.toList ρ))[i]? + = (V.positions x (BitString.toList ρ'))[i']? then + [[⟨false, varIdx t Q ρ i⟩, ⟨true, varIdx t Q ρ' i'⟩], + [⟨true, varIdx t Q ρ i⟩, ⟨false, varIdx t Q ρ' i'⟩]] + else [] + +/-- For each coin string, a clause blocking every answer vector the verdict +rejects. The verdict arrives as a Boolean function, which is the form a +polynomial-time decision procedure takes. -/ +def acceptClauses (g : List Bool → Bool) (t Q : ℕ) (x : List Bool) : List Clause := + (coinList t).flatMap fun ρ => + ((allVecs (V.positions x (BitString.toList ρ)).length).filter fun b => + !g (pair (pair x (BitString.toList ρ)) b)).map fun b => + (List.range (V.positions x (BitString.toList ρ)).length).map fun i => + (⟨!(b.getD i false), varIdx t Q ρ i⟩ : Lit) + +/-- **The formula of a verifier on an input.** -/ +def toCNF (g : List Bool → Bool) (t Q : ℕ) (x : List Bool) : CNF := + V.consClauses t Q x ++ V.acceptClauses g t Q x + +/-! ### The reduction, at the level of membership -/ + +/-- The query budget used for a given input: one more than the bound, so that +the stride is positive even when the verifier makes no queries. -/ +def budget (q : ℕ → ℕ) (x : List Bool) : ℕ := q x.length + 1 + +end PCPVerifier + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/PadGraph.lean b/Complexitylib/Classes/PCP/Internal/PadGraph.lean new file mode 100644 index 00000000..c9d524d0 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/PadGraph.lean @@ -0,0 +1,118 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.ConstraintGraph + +/-! +# Padding a constraint graph + +A `PCP` verifier tosses a number of coins that depends on the input's *length* +alone, and it uses them to pick an edge. So the graph it reads must have a +number of edges that depends on the length alone — which the graph of a formula +does not. + +Padding fixes that: extra self-loops at vertex `0`, each carrying the constraint +that is always true. They change nothing about satisfiability, and they let the +edge count be pushed up to any size a length determines. + +## Main definitions + +- `Complexity.ConstraintGraph.padGraph` — the graph with extra trivial edges + +## Main results + +- `Complexity.ConstraintGraph.satisfiable_padGraph_iff` — padding preserves + satisfiability +-/ + +@[expose] public section + +namespace Complexity + +namespace ConstraintGraph + +variable {α : Type} + +/-- `G` with trivial self-loops added until it has at least `n` edges. -/ +def padGraph (G : ConstraintGraph α) (hv : 0 < G.numVerts) (n : ℕ) : ConstraintGraph α where + numVerts := G.numVerts + numEdges := max n G.numEdges + tail e := if h : e.val < G.numEdges then G.tail ⟨e.val, h⟩ else ⟨0, hv⟩ + head e := if h : e.val < G.numEdges then G.head ⟨e.val, h⟩ else ⟨0, hv⟩ + rel e := if h : e.val < G.numEdges then G.rel ⟨e.val, h⟩ else fun _ _ => true + +variable {G : ConstraintGraph α} {hv : 0 < G.numVerts} {n : ℕ} + +@[simp] theorem numVerts_padGraph : (G.padGraph hv n).numVerts = G.numVerts := rfl + +@[simp] theorem numEdges_padGraph : (G.padGraph hv n).numEdges = max n G.numEdges := rfl + +theorem tail_padGraph_of_lt {e : ℕ} (he : e < (G.padGraph hv n).numEdges) + (h : e < G.numEdges) : + ((G.padGraph hv n).tail ⟨e, he⟩).val = (G.tail ⟨e, h⟩).val := by + show (dite _ _ _ : Fin G.numVerts).val = _ + rw [dif_pos h] + +theorem head_padGraph_of_lt {e : ℕ} (he : e < (G.padGraph hv n).numEdges) + (h : e < G.numEdges) : + ((G.padGraph hv n).head ⟨e, he⟩).val = (G.head ⟨e, h⟩).val := by + show (dite _ _ _ : Fin G.numVerts).val = _ + rw [dif_pos h] + +theorem rel_padGraph_of_lt {e : ℕ} (he : e < (G.padGraph hv n).numEdges) + (h : e < G.numEdges) : + (G.padGraph hv n).rel ⟨e, he⟩ = G.rel ⟨e, h⟩ := by + show (dite _ _ _ : α → α → Bool) = _ + rw [dif_pos h] + +theorem tail_padGraph_of_ge {e : ℕ} (he : e < (G.padGraph hv n).numEdges) + (h : ¬ e < G.numEdges) : ((G.padGraph hv n).tail ⟨e, he⟩).val = 0 := by + show (dite _ _ _ : Fin G.numVerts).val = _ + rw [dif_neg h] + +theorem head_padGraph_of_ge {e : ℕ} (he : e < (G.padGraph hv n).numEdges) + (h : ¬ e < G.numEdges) : ((G.padGraph hv n).head ⟨e, he⟩).val = 0 := by + show (dite _ _ _ : Fin G.numVerts).val = _ + rw [dif_neg h] + +theorem rel_padGraph_of_ge {e : ℕ} (he : e < (G.padGraph hv n).numEdges) + (h : ¬ e < G.numEdges) : + (G.padGraph hv n).rel ⟨e, he⟩ = fun _ _ => true := by + show (dite _ _ _ : α → α → Bool) = _ + rw [dif_neg h] + +/-- **Padding preserves satisfiability.** -/ +theorem satisfiable_padGraph_iff : (G.padGraph hv n).Satisfiable ↔ G.Satisfiable := by + constructor + · rintro ⟨a, ha⟩ + refine ⟨a, fun e => ?_⟩ + have hlt : e.val < (G.padGraph hv n).numEdges := + lt_of_lt_of_le e.isLt (le_max_right _ _) + have h := ha ⟨e.val, hlt⟩ + rw [Satisfies, satisfies] at h ⊢ + rw [rel_padGraph_of_lt hlt e.isLt] at h + rw [show (⟨e.val, e.isLt⟩ : Fin G.numEdges) = e from rfl] at h + rw [← h] + congr 1 + · exact congrArg a (Fin.ext (tail_padGraph_of_lt hlt e.isLt)).symm + · exact congrArg a (Fin.ext (head_padGraph_of_lt hlt e.isLt)).symm + · rintro ⟨a, ha⟩ + refine ⟨a, fun e => ?_⟩ + rw [Satisfies, satisfies] + by_cases h : e.val < G.numEdges + · have hb := ha ⟨e.val, h⟩ + rw [Satisfies, satisfies] at hb + rw [rel_padGraph_of_lt e.isLt h] + rw [show a ((G.padGraph hv n).tail e) = a (G.tail ⟨e.val, h⟩) from + congrArg a (Fin.ext (tail_padGraph_of_lt e.isLt h)), + show a ((G.padGraph hv n).head e) = a (G.head ⟨e.val, h⟩) from + congrArg a (Fin.ext (head_padGraph_of_lt e.isLt h))] + exact hb + · rw [rel_padGraph_of_ge e.isLt h] + +end ConstraintGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/PermArith.lean b/Complexitylib/Classes/PCP/Internal/PermArith.lean new file mode 100644 index 00000000..a9f6fa1d --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/PermArith.lean @@ -0,0 +1,254 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Mathlib.Data.Nat.Choose.Bounds +public import Mathlib.Data.Nat.Factorial.BigOperators +public import Mathlib.Analysis.Complex.ExponentialBounds +public import Mathlib.Tactic + +/-! +# Arithmetic for the expander counting bound + +Four elementary estimates, all in `ℕ`, which together turn the permutation +count of `PermCount` into a bound small enough to survive a union bound over +all vertex sets. + +* A single binomial term is at most the whole binomial sum: `C(s,t) 2^{s-t} ≤ 3^s`. +* Descending factorials compare like powers: `descFactorial s k · n^k ≤ s^k · + descFactorial n k` when `s ≤ n` — this is `(s/n)^k` in disguise, and it is + where the smallness of a set of at most half the vertices enters. +* `descFactorial n k · (n-k)! = n!`, so the count is a fraction of `n!`. +* `C(n,s) s^s ≤ 3^s n^s`, the usual `(e n / s)^s` bound with `e` replaced by + the integer `3`. Its one analytic ingredient is `(1 + 1/m)^m ≤ e < 3`. + +## Main results + +- `Complexity.choose_mul_two_pow_le` +- `Complexity.descFactorial_mul_pow_le` +- `Complexity.descFactorial_mul_factorial_sub` +- `Complexity.choose_mul_pow_self_le` +-/ + +@[expose] public section + +namespace Complexity + +open Finset + +/-- **One term of a binomial sum.** -/ +theorem choose_mul_two_pow_le (s t : ℕ) (ht : t ≤ s) : + s.choose t * 2 ^ (s - t) ≤ 3 ^ s := by + have hexp : (3 : ℕ) ^ s = ∑ k ∈ range (s + 1), 1 ^ k * 2 ^ (s - k) * s.choose k := by + have := add_pow (1 : ℕ) 2 s + norm_num at this ⊢ + exact this + rw [hexp] + have hmem : t ∈ range (s + 1) := Finset.mem_range.2 (by omega) + calc s.choose t * 2 ^ (s - t) = 1 ^ t * 2 ^ (s - t) * s.choose t := by ring + _ ≤ _ := Finset.single_le_sum + (f := fun k => 1 ^ k * 2 ^ (s - k) * s.choose k) (fun k _ => Nat.zero_le _) hmem + +/-- **Descending factorials compare like powers.** -/ +theorem descFactorial_mul_pow_le {s n : ℕ} (hsn : s ≤ n) (k : ℕ) : + s.descFactorial k * n ^ k ≤ s ^ k * n.descFactorial k := by + have hn : n ^ k = ∏ _i ∈ range k, n := by simp + have hs : s ^ k = ∏ _i ∈ range k, s := by simp + rw [Nat.descFactorial_eq_prod_range, Nat.descFactorial_eq_prod_range, hn, hs, + ← Finset.prod_mul_distrib, ← Finset.prod_mul_distrib] + refine Finset.prod_le_prod' fun i _ => ?_ + have h : (s - i) * n ≤ s * (n - i) := by + rcases le_or_gt i s with hi | hi + · have h1 : (s - i) * n = s * n - i * n := by + rw [Nat.sub_mul] + have h2 : s * (n - i) = s * n - s * i := by + rw [Nat.mul_sub] + rw [h1, h2] + have : s * i ≤ i * n := by + rw [mul_comm] + exact Nat.mul_le_mul_left _ hsn + omega + · have : s - i = 0 := by omega + rw [this, zero_mul] + exact Nat.zero_le _ + exact h + +/-- **The descending factorial is a fraction of the factorial.** -/ +theorem descFactorial_mul_factorial_sub {n k : ℕ} (hk : k ≤ n) : + n.descFactorial k * Nat.factorial (n - k) = Nat.factorial n := by + rw [Nat.descFactorial_eq_factorial_mul_choose] + calc Nat.factorial k * n.choose k * Nat.factorial (n - k) + = n.choose k * Nat.factorial k * Nat.factorial (n - k) := by ring + _ = Nat.factorial n := Nat.choose_mul_factorial_mul_factorial hk + +/-! ### The `(3 n / s)^s` bound -/ + +private theorem succ_pow_le_three_mul (m : ℕ) : ((m : ℝ) + 1) ^ m ≤ 3 * (m : ℝ) ^ m := by + rcases Nat.eq_zero_or_pos m with rfl | hm + · norm_num + have hm0 : (0 : ℝ) < m := by exact_mod_cast hm + have h1 : (1 : ℝ) + 1 / m ≤ Real.exp (1 / m) := by + have := Real.add_one_le_exp (1 / (m : ℝ)) + linarith + have h2 : ((1 : ℝ) + 1 / m) ^ m ≤ Real.exp 1 := by + calc ((1 : ℝ) + 1 / m) ^ m ≤ (Real.exp (1 / m)) ^ m := + pow_le_pow_left₀ (by positivity) h1 m + _ = Real.exp ((m : ℝ) * (1 / m)) := by + rw [← Real.exp_nat_mul] + _ = Real.exp 1 := by + rw [mul_one_div, div_self (ne_of_gt hm0)] + have h3 : Real.exp 1 < 3 := by + have := Real.exp_one_lt_d9 + linarith + have hfac : ((m : ℝ) + 1) ^ m = (m : ℝ) ^ m * ((1 : ℝ) + 1 / m) ^ m := by + rw [← mul_pow] + congr 1 + field_simp + rw [hfac] + have hmm : (0 : ℝ) ≤ (m : ℝ) ^ m := by positivity + nlinarith [h2, h3, hmm] + +/-- **`s^s ≤ 3^s s!`**, the integer form of `s! ≥ (s/e)^s`. -/ +theorem pow_self_le_three_pow_mul_factorial (s : ℕ) : + s ^ s ≤ 3 ^ s * Nat.factorial s := by + induction s with + | zero => simp + | succ m ih => + have hstep : (m + 1) ^ m ≤ 3 * m ^ m := by + have := succ_pow_le_three_mul m + exact_mod_cast this + calc (m + 1) ^ (m + 1) = (m + 1) * (m + 1) ^ m := by ring + _ ≤ (m + 1) * (3 * m ^ m) := Nat.mul_le_mul_left _ hstep + _ ≤ (m + 1) * (3 * (3 ^ m * Nat.factorial m)) := + Nat.mul_le_mul_left _ (Nat.mul_le_mul_left _ ih) + _ = 3 ^ (m + 1) * Nat.factorial (m + 1) := by + rw [Nat.factorial_succ, pow_succ] + ring + +/-- **The `(3 n / s)^s` bound on a binomial coefficient.** -/ +theorem choose_mul_pow_self_le (n s : ℕ) : n.choose s * s ^ s ≤ 3 ^ s * n ^ s := by + calc n.choose s * s ^ s ≤ n.choose s * (3 ^ s * Nat.factorial s) := + Nat.mul_le_mul_left _ (pow_self_le_three_pow_mul_factorial s) + _ = 3 ^ s * (Nat.factorial s * n.choose s) := by ring + _ = 3 ^ s * n.descFactorial s := by rw [Nat.descFactorial_eq_factorial_mul_choose] + _ ≤ 3 ^ s * n ^ s := Nat.mul_le_mul_left _ (Nat.descFactorial_le_pow n s) + +/-! ### The per-set estimate -/ + +/-- The numeric heart: `2^{2s} 3^{31s} ≤ 2^{60k}` whenever `9 s ≤ 10 k`. -/ +theorem two_pow_three_pow_le {s k : ℕ} (h9 : 9 * s ≤ 10 * k) : + 2 ^ (2 * s) * 3 ^ (31 * s) ≤ 2 ^ (60 * k) := by + have hbase : (2 : ℕ) ^ 20 * 3 ^ 310 ≤ 2 ^ 540 := by + have hb1 : (3 : ℕ) ^ 31 ≤ 2 ^ 52 := by norm_num + have hb2 : (3 : ℕ) ^ 310 ≤ 2 ^ 520 := by + calc (3 : ℕ) ^ 310 = ((3 : ℕ) ^ 31) ^ 10 := by rw [← pow_mul] + _ ≤ ((2 : ℕ) ^ 52) ^ 10 := Nat.pow_le_pow_left hb1 10 + _ = 2 ^ 520 := by rw [← pow_mul] + calc (2 : ℕ) ^ 20 * 3 ^ 310 ≤ 2 ^ 20 * 2 ^ 520 := Nat.mul_le_mul_left _ hb2 + _ = 2 ^ 540 := by rw [← pow_add] + have hL : ((2 : ℕ) ^ (2 * s) * 3 ^ (31 * s)) ^ 10 = ((2 : ℕ) ^ 20 * 3 ^ 310) ^ s := by + rw [mul_pow, mul_pow, ← pow_mul, ← pow_mul, ← pow_mul, ← pow_mul] + congr 2 <;> ring + have hR : (((2 : ℕ) ^ (60 * k)) ^ 10) = 2 ^ (600 * k) := by + rw [← pow_mul] + congr 1 + ring + have hstep : ((2 : ℕ) ^ 20 * 3 ^ 310) ^ s ≤ ((2 : ℕ) ^ 540) ^ s := + Nat.pow_le_pow_left hbase s + have hR2 : ((2 : ℕ) ^ 540) ^ s ≤ 2 ^ (600 * k) := by + rw [← pow_mul] + exact Nat.pow_le_pow_right (by norm_num) (by omega) + have hfin : ((2 : ℕ) ^ (2 * s) * 3 ^ (31 * s)) ^ 10 ≤ ((2 : ℕ) ^ (60 * k)) ^ 10 := by + rw [hL, hR] + exact le_trans hstep hR2 + exact (Nat.pow_le_pow_iff_left (by norm_num)).1 hfin + +/-- **The estimate for one vertex set**, with the permutation count abstracted. +The hypothesis is what `PermCount` supplies, in the form `descFactorial` and +`choose` bounds put it; the conclusion leaves a factor `2^s` of room for the +union bound over all sets of size `s`. -/ +theorem key_estimate {n s k B : ℕ} (hs : 1 ≤ s) (hsn : 2 * s ≤ n) (h9 : 9 * s ≤ 10 * k) + (hB : B * 2 ^ k * n ^ k ≤ 3 ^ s * s ^ k * Nat.factorial n) : + 2 ^ s * (n.choose s * B ^ 30) ≤ Nat.factorial n ^ 30 := by + obtain ⟨m, hm⟩ : ∃ m, k * 30 = s + m := ⟨k * 30 - s, by omega⟩ + have hsspos : 0 < s ^ s := pow_pos (by omega) s + have hnum := two_pow_three_pow_le h9 + have h2 : 2 ^ s * n.choose s * 3 ^ (s * 30) * s ^ (k * 30) ≤ (2 * n) ^ (k * 30) := by + refine Nat.le_of_mul_le_mul_right ?_ hsspos + have hD : n.choose s * s ^ s ≤ 3 ^ s * n ^ s := choose_mul_pow_self_le n s + have hcancel : 2 ^ s * 3 ^ (31 * s) ≤ 2 ^ (30 * k + m) := by + refine Nat.le_of_mul_le_mul_left ?_ (show 0 < 2 ^ s by positivity) + have hLl : 2 ^ s * (2 ^ s * 3 ^ (31 * s)) = 2 ^ (2 * s) * 3 ^ (31 * s) := by + rw [← mul_assoc, ← pow_add] + congr 2 + omega + have hRr : 2 ^ s * 2 ^ (30 * k + m) = 2 ^ (60 * k) := by + rw [← pow_add] + congr 1 + omega + rw [hLl, hRr] + exact hnum + have hsm : 2 ^ s * 3 ^ (31 * s) * s ^ m ≤ 2 ^ (30 * k) * n ^ m := by + calc 2 ^ s * 3 ^ (31 * s) * s ^ m ≤ 2 ^ (30 * k + m) * s ^ m := + Nat.mul_le_mul_right _ hcancel + _ = 2 ^ (30 * k) * (2 ^ m * s ^ m) := by rw [pow_add]; ring + _ = 2 ^ (30 * k) * (2 * s) ^ m := by rw [mul_pow] + _ ≤ 2 ^ (30 * k) * n ^ m := Nat.mul_le_mul_left _ (Nat.pow_le_pow_left hsn m) + have hsk : s ^ (k * 30) = s ^ s * s ^ m := by rw [hm, pow_add] + have hnk : (2 * n) ^ (k * 30) = 2 ^ (30 * k) * (n ^ s * n ^ m) := by + have e1 : (2 : ℕ) ^ (k * 30) = 2 ^ (30 * k) := by congr 1; ring + rw [mul_pow, e1, hm, pow_add] + have h3 : (3 : ℕ) ^ (s * 30) * 3 ^ s = 3 ^ (31 * s) := by + rw [← pow_add] + congr 1 + ring + calc 2 ^ s * n.choose s * 3 ^ (s * 30) * s ^ (k * 30) * s ^ s + = (2 ^ s * 3 ^ (s * 30) * s ^ (k * 30)) * (n.choose s * s ^ s) := by ring + _ ≤ (2 ^ s * 3 ^ (s * 30) * s ^ (k * 30)) * (3 ^ s * n ^ s) := + Nat.mul_le_mul_left _ hD + _ = (2 ^ s * (3 ^ (s * 30) * 3 ^ s) * s ^ m) * (s ^ s * n ^ s) := by + rw [hsk]; ring + _ = (2 ^ s * 3 ^ (31 * s) * s ^ m) * (s ^ s * n ^ s) := by rw [h3] + _ ≤ (2 ^ (30 * k) * n ^ m) * (s ^ s * n ^ s) := Nat.mul_le_mul_right _ hsm + _ = (2 * n) ^ (k * 30) * s ^ s := by rw [hnk]; ring + have h1' : B ^ 30 * (2 * n) ^ (k * 30) + ≤ 3 ^ (s * 30) * s ^ (k * 30) * Nat.factorial n ^ 30 := by + have hpow := Nat.pow_le_pow_left hB 30 + have hBpow : B ^ 30 * (2 * n) ^ (k * 30) = (B * 2 ^ k * n ^ k) ^ 30 := by + simp only [mul_pow, ← pow_mul] + ring + have hRpow : (3 ^ s * s ^ k * Nat.factorial n) ^ 30 + = 3 ^ (s * 30) * s ^ (k * 30) * Nat.factorial n ^ 30 := by + simp only [mul_pow, ← pow_mul] + rw [hBpow, ← hRpow] + exact hpow + have hpos : 0 < (2 * n) ^ (k * 30) := pow_pos (by omega) _ + refine Nat.le_of_mul_le_mul_right ?_ hpos + calc 2 ^ s * (n.choose s * B ^ 30) * (2 * n) ^ (k * 30) + = (2 ^ s * n.choose s) * (B ^ 30 * (2 * n) ^ (k * 30)) := by ring + _ ≤ (2 ^ s * n.choose s) * (3 ^ (s * 30) * s ^ (k * 30) * Nat.factorial n ^ 30) := + Nat.mul_le_mul_left _ h1' + _ = (2 ^ s * n.choose s * 3 ^ (s * 30) * s ^ (k * 30)) * Nat.factorial n ^ 30 := by ring + _ ≤ (2 * n) ^ (k * 30) * Nat.factorial n ^ 30 := Nat.mul_le_mul_right _ h2 + _ = Nat.factorial n ^ 30 * (2 * n) ^ (k * 30) := by ring + +/-- **The counting hypothesis of `key_estimate`,** as `PermCount` produces it. -/ +theorem count_bound {n s t : ℕ} (hsn : s ≤ n) (hts : t ≤ s) (hkn : s - t ≤ n) : + (s.choose t * s.descFactorial (s - t) * Nat.factorial (n - (s - t))) * 2 ^ (s - t) * n ^ (s - t) + ≤ 3 ^ s * s ^ (s - t) * Nat.factorial n := by + set k := s - t with hk + have hA : s.choose t * 2 ^ k ≤ 3 ^ s := choose_mul_two_pow_le s t hts + have hBb : s.descFactorial k * n ^ k ≤ s ^ k * n.descFactorial k := + descFactorial_mul_pow_le hsn k + have hC : n.descFactorial k * Nat.factorial (n - k) = Nat.factorial n := + descFactorial_mul_factorial_sub hkn + calc (s.choose t * s.descFactorial k * Nat.factorial (n - k)) * 2 ^ k * n ^ k + = (s.choose t * 2 ^ k) * (s.descFactorial k * n ^ k) * Nat.factorial (n - k) := by ring + _ ≤ 3 ^ s * (s ^ k * n.descFactorial k) * Nat.factorial (n - k) := + Nat.mul_le_mul_right _ (Nat.mul_le_mul hA hBb) + _ = 3 ^ s * s ^ k * (n.descFactorial k * Nat.factorial (n - k)) := by ring + _ = 3 ^ s * s ^ k * Nat.factorial n := by rw [hC] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/PermCount.lean b/Complexitylib/Classes/PCP/Internal/PermCount.lean new file mode 100644 index 00000000..c479ea2f --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/PermCount.lean @@ -0,0 +1,212 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Mathlib.Combinatorics.Enumerative.DoubleCounting +public import Mathlib.Data.Fintype.CardEmbedding +public import Mathlib.Data.Nat.Choose.Bounds +public import Mathlib.Tactic + +/-! +# Counting permutations that keep a set inside a set + +The expander existence proof needs one combinatorial estimate: of the `n!` +permutations of `Fin n`, at most `descFactorial s k · (n - k)!` map a given +`k`-element set inside a given `s`-element set. + +The proof is the obvious one, made precise. Restricting a permutation to `K` +gives an injection into `S`; there are `descFactorial s k` of those. Two +permutations with the same restriction differ only outside `K`, where they are +injections from an `(n - k)`-set into the complement of the common image — +another `(n - k)`-set — so each restriction is shared by at most `(n - k)!` +permutations. + +## Main results + +- `Complexity.card_perm_mapsTo_le` — the estimate +- `Complexity.card_perm_escape_le` — its consequence for the escape count: + few permutations move only a small part of `S` out of `S` +-/ + +@[expose] public section + +namespace Complexity + +open Finset + +variable {n : ℕ} + +/-- How many points of `S` the permutation `σ` sends outside `S`. -/ +noncomputable def escape (σ : Equiv.Perm (Fin n)) (S : Finset (Fin n)) : ℕ := + (S.filter fun v => σ v ∉ S).card + +/-! ### Restrictions of a permutation -/ + +/-- The permutations mapping `K` into `S`. -/ +noncomputable def permsInto (S K : Finset (Fin n)) : Finset (Equiv.Perm (Fin n)) := + Finset.univ.filter fun σ => ∀ v ∈ K, σ v ∈ S + +/-- The restrictions that can occur: injective maps of `K` into `S`. -/ +noncomputable def restrs (S K : Finset (Fin n)) : Finset ({x // x ∈ K} → Fin n) := + Finset.univ.filter fun f => Function.Injective f ∧ ∀ v, f v ∈ S + +theorem mem_restrs {S K : Finset (Fin n)} {f : {x // x ∈ K} → Fin n} (hf : f ∈ restrs S K) : + Function.Injective f ∧ ∀ v, f v ∈ S := by + have := hf + rw [restrs, Finset.mem_filter] at this + exact this.2 + +/-- **There are few restrictions.** -/ +theorem card_restrs_le (S K : Finset (Fin n)) : + (restrs S K).card ≤ S.card.descFactorial K.card := by + classical + have hinj : Function.Injective (fun f : {f // f ∈ restrs S K} => + (⟨fun v => ⟨f.1 v, (mem_restrs f.2).2 v⟩, fun a b hab => + (mem_restrs f.2).1 (congrArg Subtype.val hab)⟩ : + {x // x ∈ K} ↪ {x // x ∈ S})) := by + intro f g h + have h' : ∀ v, f.1 v = g.1 v := by + intro v + have := DFunLike.congr_fun h v + exact congrArg Subtype.val this + exact Subtype.ext (funext h') + have hle := Fintype.card_le_of_injective _ hinj + rwa [Fintype.card_coe, Fintype.card_embedding_eq, Fintype.card_coe, Fintype.card_coe] at hle + +/-- The values of a restriction. -/ +noncomputable def restrImage {K : Finset (Fin n)} (f : {x // x ∈ K} → Fin n) : Finset (Fin n) := + Finset.univ.image f + +theorem card_restrImage {S K : Finset (Fin n)} {f : {x // x ∈ K} → Fin n} + (hf : f ∈ restrs S K) : (restrImage f).card = K.card := by + rw [restrImage, Finset.card_image_of_injective _ (mem_restrs hf).1, Finset.card_univ, + Fintype.card_coe] + +/-- **Each restriction is shared by few permutations.** -/ +theorem card_fiber_le (S K : Finset (Fin n)) (f : {x // x ∈ K} → Fin n) + (hf : f ∈ restrs S K) : + ((permsInto S K).filter fun σ => (fun v : {x // x ∈ K} => σ v) = f).card + ≤ Nat.factorial (n - K.card) := by + classical + set R : Finset (Fin n) := Finset.univ \ restrImage f with hR + have hRcard : R.card = n - K.card := by + rw [hR, Finset.card_sdiff, Finset.inter_univ, Finset.card_univ, Fintype.card_fin, + card_restrImage hf] + -- a permutation in the fibre maps the complement of `K` into `R` + have hmaps : ∀ σ ∈ (permsInto S K).filter fun σ => (fun v : {x // x ∈ K} => σ v) = f, + ∀ v : {x // x ∉ K}, σ v.1 ∈ R := by + intro σ hσ v + rw [Finset.mem_filter] at hσ + rw [hR, Finset.mem_sdiff] + refine ⟨Finset.mem_univ _, ?_⟩ + rw [restrImage, Finset.mem_image] + rintro ⟨w, -, hw⟩ + have hfw : f w = σ w.1 := (congrFun hσ.2 w).symm + rw [hfw] at hw + have : (w : Fin n) = v.1 := σ.injective hw + exact v.2 (this ▸ w.2) + have hinj : Function.Injective (fun σ : {σ // σ ∈ (permsInto S K).filter + fun σ => (fun v : {x // x ∈ K} => σ v) = f} => + (⟨fun v => ⟨σ.1 v.1, hmaps σ.1 σ.2 v⟩, fun a b hab => by + have : σ.1 a.1 = σ.1 b.1 := congrArg Subtype.val hab + exact Subtype.ext (σ.1.injective this)⟩ : + {x // x ∉ K} ↪ {x // x ∈ R})) := by + intro σ τ h + have hout : ∀ v : Fin n, v ∉ K → σ.1 v = τ.1 v := by + intro v hv + have := DFunLike.congr_fun h ⟨v, hv⟩ + exact congrArg Subtype.val this + have hin : ∀ v : Fin n, v ∈ K → σ.1 v = τ.1 v := by + intro v hv + have hσ := σ.2 + have hτ := τ.2 + rw [Finset.mem_filter] at hσ hτ + have h1 : σ.1 v = f ⟨v, hv⟩ := congrFun hσ.2 ⟨v, hv⟩ + have h2 : τ.1 v = f ⟨v, hv⟩ := congrFun hτ.2 ⟨v, hv⟩ + rw [h1, h2] + refine Subtype.ext (Equiv.ext fun v => ?_) + by_cases hv : v ∈ K + · exact hin v hv + · exact hout v hv + have hcompl : Fintype.card {x : Fin n // x ∉ K} = n - K.card := by + have h := Fintype.card_subtype_compl (p := fun x : Fin n => x ∈ K) + rw [Fintype.card_fin, Fintype.card_coe] at h + exact h + have hle := Fintype.card_le_of_injective _ hinj + rw [Fintype.card_coe, Fintype.card_embedding_eq, Fintype.card_coe] at hle + rw [hRcard, hcompl, Nat.descFactorial_self] at hle + exact hle + +/-- **The estimate.** -/ +theorem card_perm_mapsTo_le (S K : Finset (Fin n)) : + (permsInto S K).card ≤ S.card.descFactorial K.card * Nat.factorial (n - K.card) := by + classical + have hfib : ∀ σ ∈ permsInto S K, (fun v : {x // x ∈ K} => σ v) ∈ restrs S K := by + intro σ hσ + rw [permsInto, Finset.mem_filter] at hσ + rw [restrs, Finset.mem_filter] + exact ⟨Finset.mem_univ _, fun a b hab => Subtype.ext (σ.injective hab), + fun v => hσ.2 v.1 v.2⟩ + rw [Finset.card_eq_sum_card_fiberwise hfib] + calc ∑ f ∈ restrs S K, ((permsInto S K).filter + fun σ => (fun v : {x // x ∈ K} => σ v) = f).card + ≤ ∑ _f ∈ restrs S K, Nat.factorial (n - K.card) := + Finset.sum_le_sum fun f hf => card_fiber_le S K f hf + _ = (restrs S K).card * Nat.factorial (n - K.card) := by + rw [Finset.sum_const, smul_eq_mul] + _ ≤ S.card.descFactorial K.card * Nat.factorial (n - K.card) := + Nat.mul_le_mul_right _ (card_restrs_le S K) + +/-! ### Permutations with little escape -/ + +/-- **Few permutations move only a little of `S` out of `S`.** Such a +permutation keeps a `(s - t)`-element subset of `S` inside `S`, and there are +few subsets and, by `card_perm_mapsTo_le`, few permutations for each. -/ +theorem card_perm_escape_le (S : Finset (Fin n)) (t : ℕ) : + (Finset.univ.filter fun σ : Equiv.Perm (Fin n) => escape σ S ≤ t).card + ≤ S.card.choose (S.card - t) + * (S.card.descFactorial (S.card - t) * Nat.factorial (n - (S.card - t))) := by + classical + set k := S.card - t with hk + have hsub : (Finset.univ.filter fun σ : Equiv.Perm (Fin n) => escape σ S ≤ t) + ⊆ (S.powersetCard k).biUnion fun K => permsInto S K := by + intro σ hσ + rw [Finset.mem_filter] at hσ + set A : Finset (Fin n) := S.filter fun v => σ v ∈ S with hA + have hcompl : A.card + escape σ S = S.card := by + rw [hA, escape] + exact Finset.card_filter_add_card_filter_not _ + have hAk : k ≤ A.card := by omega + obtain ⟨K, hKA, hKcard⟩ := Finset.exists_subset_card_eq hAk + rw [Finset.mem_biUnion] + refine ⟨K, ?_, ?_⟩ + · rw [Finset.mem_powersetCard] + refine ⟨fun v hv => ?_, hKcard⟩ + have := hKA hv + rw [hA, Finset.mem_filter] at this + exact this.1 + · rw [permsInto, Finset.mem_filter] + refine ⟨Finset.mem_univ _, fun v hv => ?_⟩ + have := hKA hv + rw [hA, Finset.mem_filter] at this + exact this.2 + refine le_trans (Finset.card_le_card hsub) ?_ + refine le_trans (Finset.card_biUnion_le) ?_ + have hbound : ∀ K ∈ S.powersetCard k, + (permsInto S K).card ≤ S.card.descFactorial k * Nat.factorial (n - k) := by + intro K hK + rw [Finset.mem_powersetCard] at hK + have := card_perm_mapsTo_le S K + rw [hK.2] at this + exact this + calc ∑ K ∈ S.powersetCard k, (permsInto S K).card + ≤ ∑ _K ∈ S.powersetCard k, S.card.descFactorial k * Nat.factorial (n - k) := + Finset.sum_le_sum hbound + _ = (S.powersetCard k).card * (S.card.descFactorial k * Nat.factorial (n - k)) := by + rw [Finset.sum_const, smul_eq_mul] + _ = S.card.choose k * (S.card.descFactorial k * Nat.factorial (n - k)) := by + rw [Finset.card_powersetCard] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/PermGraph.lean b/Complexitylib/Classes/PCP/Internal/PermGraph.lean new file mode 100644 index 00000000..f0f99c38 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/PermGraph.lean @@ -0,0 +1,145 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.Cheeger +public import Complexitylib.Classes.PCP.Internal.PermCount + +/-! +# Graphs built from permutations + +A tuple of `D` permutations of `Fin n` gives a `2 D`-regular graph: at each +vertex, every permutation contributes a forward dart to its image and a +backward dart to its preimage. Reversal swaps the two, so the rotation map is +an involution with no bookkeeping. + +This is the shape in which the expander family is obtained: the permutations +are chosen by counting (a random tuple works), and this module supplies the +translation from a statement about permutations — *some* permutation moves a +constant fraction of any small set out of itself — to the edge expansion the +Cheeger bound consumes. + +## Main definitions + +- `Complexity.RegGraph.permsGraph` — the `2 D`-regular graph of a tuple +- `Complexity.escape` — how many points of a set a permutation moves out of it + +## Main results + +- `Complexity.RegGraph.edgeExpansion_permsGraph` — a lower bound on escape for + every small set gives edge expansion +-/ + +@[expose] public section + +namespace Complexity + +variable {n D : ℕ} + +namespace RegGraph + +/-- The rotation map of a tuple of permutations: a forward dart becomes the +matching backward dart at the image, and conversely. -/ +def permsRot (σ : Fin D → Equiv.Perm (Fin n)) : + Fin n × (Fin D × Bool) → Fin n × (Fin D × Bool) := + fun p => if p.2.2 then ((σ p.2.1).symm p.1, (p.2.1, false)) + else (σ p.2.1 p.1, (p.2.1, true)) + +theorem permsRot_involutive (σ : Fin D → Equiv.Perm (Fin n)) : + Function.Involutive (permsRot σ) := by + intro p + obtain ⟨v, i, b⟩ := p + cases b with + | false => simp [permsRot] + | true => simp [permsRot] + +/-- **The graph of a tuple of permutations**, of degree `2 D`. -/ +def permsGraph (hD : 0 < D) (σ : Fin D → Equiv.Perm (Fin n)) : RegGraph where + V := Fin n + D := Fin D × Bool + decEqV := inferInstance + decEqD := inferInstance + fintypeV := inferInstance + fintypeD := inferInstance + nonemptyD := ⟨(⟨0, hD⟩, false)⟩ + rot := permsRot σ + rot_involutive := permsRot_involutive σ + +@[simp] theorem order_permsGraph (hD : 0 < D) (σ : Fin D → Equiv.Perm (Fin n)) : + (permsGraph hD σ).order = n := Fintype.card_fin n + +theorem deg_permsGraph (hD : 0 < D) (σ : Fin D → Equiv.Perm (Fin n)) : + (permsGraph hD σ).deg = 2 * D := by + show Fintype.card (Fin D × Bool) = 2 * D + rw [Fintype.card_prod, Fintype.card_fin, Fintype.card_bool] + ring + +/-- **Forward darts leave.** The darts of the graph that leave `S` include, for +every permutation, one for each point of `S` that permutation moves out. -/ +theorem sum_escape_le_dartsBetween (hD : 0 < D) (σ : Fin D → Equiv.Perm (Fin n)) + (S : Finset (Fin n)) : + ∑ i : Fin D, escape (σ i) S + ≤ ((permsGraph hD σ).dartsBetween S Sᶜ).card := by + classical + set E : Finset (Fin D × Fin n) := + Finset.univ.filter fun p => p.2 ∈ S ∧ σ p.1 p.2 ∉ S with hE + have hone : ∀ i : Fin D, (∑ v : Fin n, if v ∈ S ∧ σ i v ∉ S then (1 : ℕ) else 0) + = escape (σ i) S := by + intro i + rw [escape, Finset.card_filter] + simp only [ite_and] + rw [Finset.sum_ite_mem, Finset.univ_inter] + have hcard : E.card = ∑ i : Fin D, escape (σ i) S := by + rw [hE, Finset.card_filter, Fintype.sum_prod_type] + exact Finset.sum_congr rfl fun i _ => hone i + rw [← hcard] + refine Finset.card_le_card_of_injOn (fun p => (p.2, (p.1, false))) (fun p hp => ?_) ?_ + · have hp' : p.2 ∈ S ∧ σ p.1 p.2 ∉ S := by simpa [hE] using hp + have hmem : ((p.2, (p.1, false)) : (permsGraph hD σ).V × (permsGraph hD σ).D) + ∈ (permsGraph hD σ).dartsBetween S Sᶜ := + Finset.mem_filter.2 ⟨Finset.mem_univ _, hp'.1, Finset.mem_compl.2 hp'.2⟩ + simpa using hmem + · intro p _ q _ h + have h1 : p.2 = q.2 := congrArg Prod.fst h + have h2 : p.1 = q.1 := congrArg (fun x => x.2.1) h + exact Prod.ext h2 h1 + +/-- **From escape to expansion.** If every nonempty set of at most half the +vertices is moved out of itself by some permutation, in at least a `1 / c` +fraction, the graph has edge expansion `1 / (2 c D)`. -/ +theorem edgeExpansion_permsGraph (hD : 0 < D) (σ : Fin D → Equiv.Perm (Fin n)) (c : ℕ) + (hc : 0 < c) + (hesc : ∀ S : Finset (Fin n), 2 * S.card ≤ n → ∃ i : Fin D, S.card ≤ c * escape (σ i) S) : + (permsGraph hD σ).EdgeExpansion (1 / (2 * (c : ℝ) * D)) := by + classical + intro S hS + rw [order_permsGraph] at hS + obtain ⟨i, hi⟩ := hesc S hS + have hcD : (0 : ℝ) < 2 * (c : ℝ) * D := by + positivity + have hsum : (escape (σ i) S : ℝ) ≤ ∑ j : Fin D, (escape (σ j) S : ℝ) := by + refine Finset.single_le_sum (f := fun j => (escape (σ j) S : ℝ)) (fun j _ => ?_) + (Finset.mem_univ i) + positivity + have hbound : (∑ j : Fin D, (escape (σ j) S : ℝ)) + ≤ (((permsGraph hD σ).dartsBetween S Sᶜ).card : ℝ) := by + have := sum_escape_le_dartsBetween hD σ S + exact_mod_cast this + have hi' : (S.card : ℝ) ≤ (c : ℝ) * (escape (σ i) S : ℝ) := by exact_mod_cast hi + rw [deg_permsGraph] + have hrw : 1 / (2 * (c : ℝ) * D) * ((2 * D : ℕ) : ℝ) * (S.card : ℝ) + = (S.card : ℝ) / (c : ℝ) := by + push_cast + field_simp + rw [hrw] + have hc' : (0 : ℝ) < c := by exact_mod_cast hc + rw [div_le_iff₀ hc'] + have hfin : (escape (σ i) S : ℝ) ≤ (((permsGraph hD σ).dartsBetween S Sᶜ).card : ℝ) := + le_trans hsum hbound + nlinarith [hi', hfin, hc'] + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/PosScan.lean b/Complexitylib/Classes/PCP/Internal/PosScan.lean new file mode 100644 index 00000000..85b442b6 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/PosScan.lean @@ -0,0 +1,142 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.HeadOps +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.PCP.Internal.DataScan +public import Complexitylib.Classes.PCP.Internal.SubsetNP +public import Complexitylib.Classes.PCP.Internal.CoinEnum + +/-! +# Reading a verifier's query list + +A `PCPVerifier` hands over its query positions only as an encoded list — that is +what `positions_mem` provides, and it is all a polynomial-time algorithm can +have, since a single position may be astronomically large. This module reads +individual entries back out of that encoding with the bracket scan. + +Two facts make the reading enough for everything downstream. A position is +recovered as its own serialization, so two positions can be compared by +comparing strings, with no arithmetic on the values; and an index past the end +of the list is recognisable, because every serialization is non-empty. + +## Main definitions + +- `Complexity.posInner` — the encoding stripped of its outer brackets +- `Complexity.posAt`, `Complexity.posCount` — one entry, and how many there are + +## Main results + +- `Complexity.posAt_eq`, `Complexity.posCount_eq` — what the scan reads +- `Complexity.posAt_eq_iff` — comparing entries compares positions +-/ + +@[expose] public section + +namespace Complexity + +/-- The serialized entries of an encoded list, with the outer brackets removed: +the string the scan consumes. -/ +def posInner (e : List Bool) : List Bool := (e.drop 1).take (e.length - 2) + +theorem posInner_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => posInner (a z)) ∈ FP := by + have hdrop : (fun z => dropOne (a z)) ∈ FP := dropOneFn_mem_FP ha + have hlen : (fun z => List.replicate ((a z).length - 2) false) ∈ FP := by + have h1 : (fun z => dropOne (dropOne (a z))) ∈ FP := dropOneFn_mem_FP hdrop + have := zeroBlockFn_mem_FP h1 + refine mem_FP_of_eq this fun z => ?_ + congr 1 + rw [dropOne, dropOne, List.length_drop, List.length_drop] + omega + have := Cobham.takeLenFn_mem_FP hlen hdrop + refine mem_FP_of_eq this fun z => ?_ + rw [posInner, dropOne, List.length_replicate] + +/-- The `i`-th entry of an encoded list, as its own serialization. -/ +noncomputable def posAt (e : List Bool) (i : ℕ) : List Bool := + DataScan.childOf DataScan.scanPoly (DataScan.scanArg i (posInner e)) + +/-- How many entries an encoded list has, in unary. -/ +noncomputable def posCount (e : List Bool) : List Bool := + DataScan.childCount DataScan.scanPoly (DataScan.scanArg 0 (posInner e)) + +theorem posAt_mem_FP {a b : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) : + (fun z => posAt (b z) (a z).length) ∈ FP := by + have harg : (fun z => DataScan.scanArg (a z).length (posInner (b z))) ∈ FP := + DataScan.scanArg_mem_FP ha (posInner_mem_FP hb) + have := mem_FP_comp harg (DataScan.childOf_mem_FP DataScan.scanPoly) + simpa using this + +theorem posCount_mem_FP {b : List Bool → List Bool} (hb : b ∈ FP) : + (fun z => posCount (b z)) ∈ FP := by + have harg : (fun z => DataScan.scanArg (([] : List Bool)).length (posInner (b z))) ∈ FP := + DataScan.scanArg_mem_FP (constFn_mem_FP []) (posInner_mem_FP hb) + have := mem_FP_comp harg (DataScan.childCount_mem_FP DataScan.scanPoly) + simpa using this + +/-! ### What the scan reads -/ + +variable {α : Type} [DataEncode α] + +theorem posInner_bitstringEncode (l : List α) : + posInner (DataEncode.bitstringEncode l) + = ((l.map DataEncode.encode).map Data.toBits).flatten := by + rw [posInner, DataEncode.bitstringEncode_def, + show DataEncode.encode l = Data.l (l.map DataEncode.encode) from rfl] + exact DataScan.inner_toBits _ + +theorem posCount_eq (l : List α) : + posCount (DataEncode.bitstringEncode l) = List.replicate l.length true := by + rw [posCount, posInner_bitstringEncode, DataScan.childCount_flatten] + simp + +theorem posAt_eq (l : List α) (i : ℕ) : + posAt (DataEncode.bitstringEncode l) i + = (((l.map DataEncode.encode)[i]?).map Data.toBits).getD [] := by + rw [posAt, posInner_bitstringEncode, DataScan.child_flatten] + +theorem posAt_eq_of_lt {l : List α} {i : ℕ} (h : i < l.length) : + posAt (DataEncode.bitstringEncode l) i + = DataEncode.bitstringEncode (l[i]'h) := by + rw [posAt_eq, List.getElem?_map, List.getElem?_eq_getElem (by simpa using h)] + rfl + +theorem posAt_eq_nil {l : List α} {i : ℕ} (h : l.length ≤ i) : + posAt (DataEncode.bitstringEncode l) i = [] := by + rw [posAt_eq, List.getElem?_map, List.getElem?_eq_none (by simpa using h)] + rfl + +theorem posAt_ne_nil {l : List α} {i : ℕ} (h : i < l.length) : + posAt (DataEncode.bitstringEncode l) i ≠ [] := by + rw [posAt_eq_of_lt h, DataEncode.bitstringEncode_def] + cases hd : DataEncode.encode (l[i]'h) with + | l xs => + rw [Data.toBits_l] + simp + +/-- **Concatenating encoded lists.** The encoding of an append is the two inner +parts, one after the other, inside a fresh pair of brackets. -/ +theorem bitstringEncode_append (l₁ l₂ : List α) : + DataEncode.bitstringEncode (l₁ ++ l₂) + = false :: (posInner (DataEncode.bitstringEncode l₁) + ++ posInner (DataEncode.bitstringEncode l₂)) ++ [true] := by + rw [posInner_bitstringEncode, posInner_bitstringEncode, + DataEncode.bitstringEncode_def, + show DataEncode.encode (l₁ ++ l₂) + = Data.l ((l₁ ++ l₂).map DataEncode.encode) from rfl, + Data.toBits_l, List.map_append, List.map_append, List.flatten_append] + simp + +/-- **Comparing entries compares positions.** The scan returns each entry's own +serialization, and that serialization determines the entry. -/ +theorem posAt_eq_iff {l l' : List α} {i i' : ℕ} (h : i < l.length) (h' : i' < l'.length) : + posAt (DataEncode.bitstringEncode l) i = posAt (DataEncode.bitstringEncode l') i' + ↔ (l[i]'h) = (l'[i']'h') := by + rw [posAt_eq_of_lt h, posAt_eq_of_lt h'] + exact ⟨fun hh => DataEncode.bitstringEncode_injective hh, fun hh => by rw [hh]⟩ + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/PositionsFP.lean b/Complexitylib/Classes/PCP/Internal/PositionsFP.lean new file mode 100644 index 00000000..aa2fc215 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/PositionsFP.lean @@ -0,0 +1,112 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.ListEncode + +/-! +# A verifier's query list from its positions + +`PCPVerifier` asks for the query list in `DataEncode` form, which is not how an +algorithm naturally describes it: an algorithm says "the `i`-th position is +this number". This module bridges the two, so that building a verifier needs +only a polynomial-time rule for each position, given in unary. + +The width a number needs is the number itself — every `v` is below `2 ^ v` — so +no logarithms are involved: the same unary value serves as both the value and +the width bound for `natEncodeFn`. + +## Main results + +- `Complexity.positions_mem_of_unary` — a unary position rule gives + `positions_mem` +-/ + +@[expose] public section + +namespace Complexity + +theorem fstBlock_length_le (z : List Bool) : (pairFst z).length ≤ z.length := by + induction z using pairFst.induct <;> simp [pairFst] <;> omega + +/-- The encoding of one position, read off a packed argument. -/ +noncomputable def posEntryFn (P : List Bool → List Bool) (w : List Bool) : List Bool := + natEncodeFn (pair (P w) (P w)) + +theorem posEntryFn_mem_FP {P : List Bool → List Bool} (hP : P ∈ FP) : + posEntryFn P ∈ FP := by + have := mem_FP_comp (Cobham.pairFn_mem_FP hP hP) natEncodeFn_mem_FP + simpa using this + +theorem posEntryFn_eq {P : List Bool → List Bool} (w : List Bool) : + posEntryFn P w = DataEncode.bitstringEncode ((P w).length) := by + rw [posEntryFn, natEncodeFn_eq, pairSnd_pair] + rw [pairSnd_pair, pairFst_pair] + exact Nat.lt_two_pow_self + +/-- **A unary position rule gives the query list.** If the number of queries and +each query position are polynomial-time computable in unary, then the encoded +query list is polynomial-time computable. -/ +theorem positions_mem_of_unary {pos : List Bool → ℕ → ℕ} {cnt : List Bool → ℕ} + (hcnt : (fun z : List Bool => List.replicate (cnt z) true) ∈ FP) + {P : List Bool → List Bool} (hP : P ∈ FP) + (hPspec : ∀ (z : List Bool) (i : ℕ), + P (pair z (List.replicate i true)) = List.replicate (pos z i) true) : + ∃ g ∈ FP, ∀ z : List Bool, + g z = DataEncode.bitstringEncode ((List.range (cnt z)).map (pos z)) := by + classical + set E := posEntryFn P with hE + have hEfp : E ∈ FP := posEntryFn_mem_FP hP + have hEspec : ∀ (z : List Bool) (i : ℕ), + E (pair z (List.replicate i true)) = DataEncode.bitstringEncode (pos z i) := by + intro z i + rw [hE, posEntryFn_eq, hPspec, List.length_replicate] + -- the loop stays polynomial + obtain ⟨pE, hpE⟩ := Cobham.output_length_poly_of_mem_FP hEfp + set p : Polynomial ℕ := + Polynomial.C 4 * Polynomial.X * (pE.comp (Polynomial.C 3 * Polynomial.X + Polynomial.C 2)) + + Polynomial.C 3 * Polynomial.X + Polynomial.C 6 with hp + have hbound : ∀ z' : List Bool, ∀ k ≤ (pairFst z').length, + ((listStep E)^[k] (pair (pair [] []) (pairSnd z'))).length + ≤ p.eval z'.length := by + intro z' k hk + have hx : (pairSnd z').length ≤ z'.length := pairSnd_length_le z' + have hf : (pairFst z').length ≤ z'.length := fstBlock_length_le z' + have hb : ∀ i < k, (E (pair (pairSnd z') (List.replicate i true))).length + ≤ pE.eval (3 * z'.length + 2) := by + intro i hi + refine le_trans (hpE _) ?_ + refine polynomial_eval_mono_nat pE ?_ + rw [pair_length, List.length_replicate] + omega + have hcat := length_entryCat_le E (pairSnd z') _ k hb + rw [listStep_iterate, pair_length, pair_length, List.length_replicate] + have hpe : p.eval z'.length + = 4 * z'.length * (pE.eval (3 * z'.length + 2)) + 3 * z'.length + 6 := by + rw [hp] + simp + rw [hpe] + have hkz : k ≤ z'.length := le_trans hk hf + have hmul : k * pE.eval (3 * z'.length + 2) + ≤ z'.length * pE.eval (3 * z'.length + 2) := Nat.mul_le_mul_right _ hkz + have hcat' : (entryCat E (pairSnd z') k).length + ≤ z'.length * pE.eval (3 * z'.length + 2) := le_trans hcat hmul + rw [show 4 * z'.length * pE.eval (3 * z'.length + 2) + = 4 * (z'.length * pE.eval (3 * z'.length + 2)) from by ring] + omega + refine ⟨fun z => listEncFn E (pair (List.replicate (cnt z) true) z), ?_, ?_⟩ + · have hpair : (fun z : List Bool => pair (List.replicate (cnt z) true) z) ∈ FP := + mem_FP_pairWithInput hcnt + have := mem_FP_comp hpair (listEncFn_mem_FP hEfp p hbound) + simpa using this + · intro z + refine listEncFn_eq_bitstringEncode _ ?_ ?_ + · rw [pairFst_pair, List.length_replicate, List.length_map, List.length_range] + · intro i hi + rw [pairSnd_pair, hEspec] + congr 1 + rw [List.getElem_map, List.getElem_range] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/Power.lean b/Complexitylib/Classes/PCP/Internal/Power.lean new file mode 100644 index 00000000..12f2450e --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/Power.lean @@ -0,0 +1,198 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.WalkPath + +/-! +# Walk reversal and the powered graph + +The graph-theoretic half of Dinur's powering step. The `t`-th power of a +regular graph `G` has the same vertices, and one edge for each **walk** of +length `t` in `G`, joining the walk's two ends. + +Making that a `RegGraph` means exhibiting the rotation map: an involution on +darts. A dart of the power is a vertex together with a tuple of `t` labels, and +its reverse is the *reversed walk* — starting at the far end and retracing the +same edges backwards. So the work here is to define reversal and prove it is an +involution. + +## How reversal is defined + +The `k`-th dart of the walk `(v, s)` is `(walkAt k, s k)`; reversing it with +`G.rot` yields the next vertex together with the label that points *back*, +called `backLabel`. The reversed walk reads those back-labels in reverse order, +using `Fin.rev` — whose own involutivity (`Fin.rev_rev`) carries most of the +index bookkeeping. + +`rot_dart` is the one computational fact everything rests on: reversing the +`k`-th dart gives `(walkAt (k+1), backLabel k)`. Applying `G.rot_involutive` to +it turns each step of the reversed walk back into a step of the original. + +## Why the power's spectral bound is `lam ^ t` + +The power's walk operator *is* the `t`-fold operator of `G`: averaging over all +`deg ^ t` walks out of a vertex is exactly `stepIter t` (`sum_walkEnd`). So the +squared-norm contraction of `RegularGraph` applies verbatim, and a graph with +`SpectralBound lam` powers up to one with `SpectralBound (lam ^ t)`. This is +what makes powering amplify the gap. + +## Main definitions + +- `RegGraph.backLabel` — the label pointing back along a dart of a walk +- `RegGraph.revWalk` — the reversed walk +- `RegGraph.power` — the `t`-th power as a `RegGraph` + +## Main results + +- `RegGraph.rot_dart` — reversing the `k`-th dart of a walk +- `RegGraph.walkAt_revWalk` — the reversed walk retraces the trajectory +- `RegGraph.walkEnd_revWalk`, `RegGraph.revWalk_revWalk` — reversal is an + involution +- `RegGraph.step_power` — the power's walk operator is `stepIter t` +- `RegGraph.spectralBound_power` — `SpectralBound lam` powers to + `SpectralBound (lam ^ t)` +- `RegGraph.deg_power`, `RegGraph.order_power` +-/ + +@[expose] public section + +namespace Complexity + +namespace RegGraph + +variable (G : RegGraph) + +/-! ### Reversing the darts of a walk -/ + +/-- The label at `walkAt (k+1)` that points back along the `k`-th dart of the +walk `(v, s)`. -/ +def backLabel {t : ℕ} (v : G.V) (s : Fin t → G.D) (k : Fin t) : G.D := + (G.rot (G.walkAt t v s k.val, s k)).2 + +/-- **The computational core.** Reversing the `k`-th dart of the walk `(v, s)` +gives the next vertex on the walk, together with the label pointing back. -/ +theorem rot_dart {t : ℕ} (v : G.V) (s : Fin t → G.D) (k : Fin t) : + G.rot (G.walkAt t v s k.val, s k) + = (G.walkAt t v s (k.val + 1), G.backLabel v s k) := by + refine Prod.ext ?_ rfl + exact (G.walkAt_succ_of_lt v s k.isLt).symm + +/-- The walk `(v, s)` reversed: it starts at the far end and reads the +back-labels of the original darts in reverse order. -/ +def revWalk {t : ℕ} (v : G.V) (s : Fin t → G.D) : Fin t → G.D := + fun j => G.backLabel v s (Fin.rev j) + +/-- The reversed walk retraces the original trajectory backwards. -/ +theorem walkAt_revWalk {t : ℕ} (v : G.V) (s : Fin t → G.D) : + ∀ k : ℕ, k ≤ t → + G.walkAt t (G.walkEnd t v s) (G.revWalk v s) k = G.walkAt t v s (t - k) := by + intro k + induction k with + | zero => intro _; simpa using (G.walkAt_self_eq_walkEnd v s).symm + | succ k ih => + intro hk + have hkt : k < t := by omega + have hk' : k ≤ t := le_of_lt hkt + have hrev : (Fin.rev (⟨k, hkt⟩ : Fin t)).val = t - (k + 1) := by + rw [Fin.val_rev] + have hrot := G.rot_dart v s (Fin.rev (⟨k, hkt⟩ : Fin t)) + have hinv := G.rot_involutive + (G.walkAt t v s (Fin.rev (⟨k, hkt⟩ : Fin t)).val, s (Fin.rev (⟨k, hkt⟩ : Fin t))) + rw [hrot] at hinv + rw [G.walkAt_succ_of_lt _ _ hkt, ih hk'] + have harith : t - k = (t - (k + 1)) + 1 := by omega + have hstep : G.revWalk v s ⟨k, hkt⟩ = G.backLabel v s (Fin.rev (⟨k, hkt⟩ : Fin t)) := rfl + rw [hstep, harith, nbr, ← hrev, hinv, hrev] + +/-- Reversing a walk lands back at its start. -/ +theorem walkEnd_revWalk {t : ℕ} (v : G.V) (s : Fin t → G.D) : + G.walkEnd t (G.walkEnd t v s) (G.revWalk v s) = v := by + have h := G.walkAt_revWalk v s t le_rfl + rw [G.walkAt_self_eq_walkEnd] at h + simpa using h + +/-- Reversal is an involution on walks. -/ +theorem revWalk_revWalk {t : ℕ} (v : G.V) (s : Fin t → G.D) : + G.revWalk (G.walkEnd t v s) (G.revWalk v s) = s := by + funext j + have hj : (Fin.rev j).val ≤ t := le_of_lt (Fin.rev j).isLt + have hwalk : G.walkAt t (G.walkEnd t v s) (G.revWalk v s) (Fin.rev j).val + = G.walkAt t v s (t - (Fin.rev j).val) := G.walkAt_revWalk v s _ hj + have harith : t - (Fin.rev j).val = j.val + 1 := by + rw [Fin.val_rev]; omega + have hlabel : G.revWalk v s (Fin.rev j) = G.backLabel v s j := by + rw [revWalk, Fin.rev_rev] + have hrot := G.rot_dart v s j + have hinv := G.rot_involutive (G.walkAt t v s j.val, s j) + rw [hrot] at hinv + calc G.revWalk (G.walkEnd t v s) (G.revWalk v s) j + = (G.rot (G.walkAt t (G.walkEnd t v s) (G.revWalk v s) (Fin.rev j).val, + G.revWalk v s (Fin.rev j))).2 := rfl + _ = (G.rot (G.walkAt t v s (j.val + 1), G.backLabel v s j)).2 := by + rw [hwalk, harith, hlabel] + _ = s j := by rw [hinv] + +/-! ### The powered graph -/ + +/-- The `t`-th power of `G`: same vertices, one edge per walk of length `t`. -/ +def power (G : RegGraph) (t : ℕ) : RegGraph where + V := G.V + D := Fin t → G.D + decEqV := G.decEqV + decEqD := inferInstance + fintypeV := G.fintypeV + fintypeD := inferInstance + nonemptyD := inferInstance + rot p := (G.walkEnd t p.1 p.2, G.revWalk p.1 p.2) + rot_involutive p := by + refine Prod.ext ?_ ?_ + · exact G.walkEnd_revWalk p.1 p.2 + · exact G.revWalk_revWalk p.1 p.2 + +@[simp] theorem V_power (t : ℕ) : (G.power t).V = G.V := rfl + +@[simp] theorem D_power (t : ℕ) : (G.power t).D = (Fin t → G.D) := rfl + +@[simp] theorem order_power (t : ℕ) : (G.power t).order = G.order := rfl + +@[simp] theorem deg_power (t : ℕ) : (G.power t).deg = G.deg ^ t := by + show Fintype.card (Fin t → G.D) = G.deg ^ t + rw [Fintype.card_fun, Fintype.card_fin] + rfl + +/-- The power's walk operator is the `t`-fold walk operator of `G`. -/ +theorem step_power (t : ℕ) (f : G.V → ℝ) (v : G.V) : + (G.power t).step f v = G.stepIter t f v := by + have hd : ((G.deg : ℝ)) ^ t ≠ 0 := pow_ne_zero _ G.deg_ne_zero + calc (G.power t).step f v + = (∑ s : Fin t → G.D, f (G.walkEnd t v s)) / ((G.deg ^ t : ℕ) : ℝ) := by + rw [step, deg_power] + rfl + _ = ((G.deg : ℝ) ^ t * G.stepIter t f v) / ((G.deg : ℝ) ^ t) := by + rw [G.sum_walkEnd f t v] + push_cast + ring_nf + _ = G.stepIter t f v := by + field_simp + +/-- **Powering amplifies the spectral gap.** -/ +theorem spectralBound_power {lam : ℝ} (h : G.SpectralBound lam) (t : ℕ) : + (G.power t).SpectralBound (lam ^ t) := by + intro f hf + show (∑ v : G.V, ((G.power t).step f v) ^ 2) ≤ (lam ^ t) ^ 2 * ∑ v : G.V, (f v) ^ 2 + have hsum : (∑ v : G.V, f v) = 0 := hf + have hbound := G.sum_sq_stepIter_le h t f hsum + have hstep : ∀ v : G.V, (G.power t).step f v = G.stepIter t f v := G.step_power t f + calc ∑ v : G.V, ((G.power t).step f v) ^ 2 + = ∑ v : G.V, (G.stepIter t f v) ^ 2 := + Finset.sum_congr rfl fun v _ => by rw [hstep v] + _ ≤ lam ^ (2 * t) * ∑ v : G.V, (f v) ^ 2 := hbound + _ = (lam ^ t) ^ 2 * ∑ v : G.V, (f v) ^ 2 := by + rw [← pow_mul, mul_comm 2 t] + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/PoweringBound.lean b/Complexitylib/Classes/PCP/Internal/PoweringBound.lean new file mode 100644 index 00000000..e373ac37 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/PoweringBound.lean @@ -0,0 +1,419 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.KilledFirstMoment +public import Mathlib.Algebra.Order.Ring.Pow + +/-! +# The powering bound, in closed form + +`powering_soundness` states the second-moment argument exactly as it falls out +of the two moment estimates: a ratio of a first-moment lower bound squared to a +second-moment upper bound, with every count left explicit. This module turns it +into the statement the amplifier needs — a lower bound on the powered system's +value as a rational function of the original value, with all the constants +isolated: + +`c² u / (c + 2 T² u + 2 T / (1 - λ)) ≤ unsatFrac (killedPow)` + +where `u` is the decoded assignment's violated fraction and +`c = (q - 1) / (4 |α|²)`. For small `u` this is linear in `u` with slope of +order `(q - 1)² (1 - λ) / (|α|⁴ T)`, and since `T` need only be linear in +`q |α|`, the slope grows with `q`: that is the amplification. For large `u` it +is bounded below by a constant. + +Two technical points are handled here. The first moment appears in the +denominator of the second-moment bound, so the ratio has to be made monotone +before the first-moment *lower* bound can be substituted in both places. And +the plurality loss must be at most half the total, which requires `T` large +enough relative to `q` and `|α|` — Bernoulli's inequality shows +`H + 1 = 4 |α| (q - 1)` suffices. + +## Main results + +- `Complexity.RegCSP.unsatFrac_killedPow_clean` — the closed-form bound +- `Complexity.RegCSP.le_unsatVal_killedPow_min` — the `min` form +- `Complexity.exists_powering_params` — a choice of `T` and `H` meeting every + side condition +-/ + +@[expose] public section + +namespace Complexity + +/-! ### Monotonicity of the Paley–Zygmund ratio -/ + +/-- `m ↦ m² / (m + P)` is increasing on `m ≥ 0` for `P > 0`. -/ +theorem sq_div_add_mono {m M P : ℝ} (hm : 0 ≤ m) (hmM : m ≤ M) (hP : 0 < P) : + m ^ 2 / (m + P) ≤ M ^ 2 / (M + P) := by + rw [div_le_div_iff₀ (by linarith) (by linarith)] + have h1 : 0 ≤ m * M * (M - m) := by + apply mul_nonneg (mul_nonneg hm (by linarith)) (by linarith) + have h2 : 0 ≤ P * ((M - m) * (M + m)) := by + apply mul_nonneg hP.le (mul_nonneg (by linarith) (by linarith)) + nlinarith [h1, h2] + +/-! ### The algebra of the bound -/ + +/-- The powering bound's algebra, over opaque real quantities: `U` violated +darts among `N · d`, first moment at least `U (q - 1) (d^T q^T / 2)² / (K² d^(T+1) q^T)`, +second moment as in `sum_sq_goodCrossings_le`. -/ +theorem powering_algebra {U N d K q lam C : ℝ} (T' : ℕ) (hN : 0 < N) (hd : 0 < d) + (hK : 0 < K) (hq : 1 ≤ q) (hlam : lam < 1) (hU : 0 < U) + (hcount : (U * ((q - 1) * (d ^ (T' + 2) * q ^ (T' + 2) / 2 + * (d ^ (T' + 2) * q ^ (T' + 2) / 2))) / (K ^ 2 * (d ^ (T' + 2 + 1) * q ^ (T' + 2)))) ^ 2 + / (U * ((q - 1) * (d ^ (T' + 2) * q ^ (T' + 2) / 2 + * (d ^ (T' + 2) * q ^ (T' + 2) / 2))) / (K ^ 2 * (d ^ (T' + 2 + 1) * q ^ (T' + 2))) + + 2 * (q ^ (T' + 2) * (d ^ T' * (((T' + 2 : ℕ) : ℝ) * ((T' + 2 : ℕ) : ℝ) * (U * U / N) + + ((T' + 2 : ℕ) : ℝ) * (1 / (1 - lam)) * (d * U))))) + ≤ C) : + ((q - 1) / (4 * K ^ 2)) ^ 2 * (U / (N * d)) + / ((q - 1) / (4 * K ^ 2) + 2 * ((T' + 2 : ℕ) : ℝ) ^ 2 * (U / (N * d)) + + 2 * ((T' + 2 : ℕ) : ℝ) / (1 - lam)) + ≤ C / (N * (d ^ (T' + 2) * q ^ (T' + 2))) := by + have hlam' : 0 < 1 - lam := by linarith + set c : ℝ := (q - 1) / (4 * K ^ 2) with hc + set u : ℝ := U / (N * d) with hu + set W : ℝ := N * (d ^ (T' + 2) * q ^ (T' + 2)) with hW + set Tr : ℝ := ((T' + 2 : ℕ) : ℝ) with hTr + set E : ℝ := 2 * Tr ^ 2 * u + 2 * Tr / (1 - lam) with hE + have hW0 : 0 < W := by rw [hW]; positivity + have hF : U * ((q - 1) * (d ^ (T' + 2) * q ^ (T' + 2) / 2 + * (d ^ (T' + 2) * q ^ (T' + 2) / 2))) / (K ^ 2 * (d ^ (T' + 2 + 1) * q ^ (T' + 2))) + = c * u * W := by + rw [hc, hu, hW] + field_simp + ring + have hP : 2 * (q ^ (T' + 2) * (d ^ T' * (Tr * Tr * (U * U / N) + Tr * (1 / (1 - lam)) * (d * U)))) + = W * (u * E) := by + rw [hE, hu, hW] + field_simp + ring + rw [hF, hP] at hcount + have hratio : (c * u * W) ^ 2 / (c * u * W + W * (u * E)) = W * (c ^ 2 * u / (c + E)) := by + field_simp + rw [hratio] at hcount + have hgoal : c + 2 * Tr ^ 2 * u + 2 * Tr / (1 - lam) = c + E := by rw [hE]; ring + rw [hgoal, div_le_div_iff₀ (by positivity) hW0] + have hcE : 0 < c + E := by positivity + have h := mul_le_mul_of_nonneg_right hcount hcE.le + have heq : c ^ 2 * u * W = W * (c ^ 2 * u / (c + E)) * (c + E) := by + rw [mul_assoc W, div_mul_cancel₀ _ hcE.ne'] + ring + rw [heq] + exact h + +namespace RegCSP + +variable {α : Type} [Fintype α] [DecidableEq α] [Nonempty α] +variable (R : RegCSP α) (q T : ℕ) (hq : 0 < q) + +/-- **The second-moment count, with the first-moment lower bound on both +sides.** -/ +theorem card_unsatDarts_ge' (A : (R.killedPow q T hq).Assignment) {Alb P : ℝ} + (hA0 : 0 ≤ Alb) + (hA : Alb ≤ ∑ z : R.graph.V × R.KLabels q T, ((R.goodCrossings q T hq A z).card : ℝ)) + (hB : ∑ z : R.graph.V × R.KLabels q T, ((R.goodCrossings q T hq A z).card : ℝ) ^ 2 + ≤ (∑ z : R.graph.V × R.KLabels q T, ((R.goodCrossings q T hq A z).card : ℝ)) + P) + (hP : 0 < P) : + Alb ^ 2 / (Alb + P) ≤ (((R.killedPow q T hq).unsatDarts A).card : ℝ) := by + classical + have hS0 : 0 ≤ ∑ z : R.graph.V × R.KLabels q T, ((R.goodCrossings q T hq A z).card : ℝ) := + Finset.sum_nonneg fun z _ => by positivity + refine le_trans (sq_div_add_mono hA0 hA hP) ?_ + exact R.card_unsatDarts_ge q T hq A hS0 le_rfl hB (by linarith) + +/-- The slope constant of the powering bound. -/ +noncomputable def powConst (q : ℕ) (α : Type) [Fintype α] : ℝ := + ((q : ℝ) - 1) / (4 * (Fintype.card α : ℝ) ^ 2) + +/-- **The powering bound in closed form.** Under the side conditions of +`powering_soundness` and with the plurality loss at most half the total, the +powered system's violated fraction is at least +`c² u / (c + 2 T² u + 2 T / (1 - λ))`, `u` the decoded assignment's violated +fraction. -/ +theorem unsatFrac_killedPow_clean (A : (R.killedPow q T hq).Assignment) {H : ℕ} + (hH : 2 * H + 1 < T) (hHT : H + 1 ≤ T) + (hsq : ∀ i ∈ Finset.range (H + 1), ∀ j ∈ Finset.range (H + 1), i + j + 1 < T) + {lam : ℝ} (hlam0 : 0 ≤ lam) (hlam1 : lam < 1) (hspec : R.graph.SpectralBound lam) + (hn : 0 < R.graph.order) (hq1 : 1 ≤ q) + (hloss : 2 * (Fintype.card α * pluralityLoss R.graph.deg q T H) ≤ R.graph.deg ^ T * q ^ T) : + powConst q α ^ 2 * (((R.unsatFrac (R.kDecode q T hq A) : ℚ) : ℝ)) + / (powConst q α + 2 * (T : ℝ) ^ 2 * (((R.unsatFrac (R.kDecode q T hq A) : ℚ) : ℝ)) + + 2 * (T : ℝ) / (1 - lam)) + ≤ (((R.killedPow q T hq).unsatFrac A : ℚ) : ℝ) := by + classical + have hN0 : (0 : ℝ) < R.graph.order := by exact_mod_cast hn + have hd0 : (0 : ℝ) < R.graph.deg := by exact_mod_cast R.graph.deg_pos + have hK0 : (0 : ℝ) < Fintype.card α := by + exact_mod_cast (Fintype.card_pos : 0 < Fintype.card α) + have hq1' : (1 : ℝ) ≤ q := by exact_mod_cast hq1 + -- the violated fraction of the decoded assignment + have hu : (((R.unsatFrac (R.kDecode q T hq A) : ℚ) : ℝ)) + = ((R.unsatDarts (R.kDecode q T hq A)).card : ℝ) / ((R.graph.order : ℝ) * R.graph.deg) := by + rw [RegCSP.unsatFrac] + push_cast + rfl + have hfrac : (((R.killedPow q T hq).unsatFrac A : ℚ) : ℝ) + = (((R.killedPow q T hq).unsatDarts A).card : ℝ) + / ((R.graph.order : ℝ) * ((R.graph.deg : ℝ) ^ T * (q : ℝ) ^ T)) := by + rw [RegCSP.unsatFrac, R.card_dart_killedPow q T hq] + push_cast + rfl + rw [hu, hfrac, powConst] + -- the case of no violated darts is trivial + by_cases hU0 : ((R.unsatDarts (R.kDecode q T hq A)).card : ℝ) = 0 + · rw [hU0] + simp only [zero_div, mul_zero] + positivity + have hUpos : (0 : ℝ) < ((R.unsatDarts (R.kDecode q T hq A)).card : ℝ) := + lt_of_le_of_ne (by positivity) (Ne.symm hU0) + -- the first-moment lower bound, simplified using the loss condition + have hle : Fintype.card α * pluralityLoss R.graph.deg q T H ≤ R.graph.deg ^ T * q ^ T := by + omega + have hhalf : (((R.graph.deg ^ T * q ^ T - Fintype.card α * pluralityLoss R.graph.deg q T H + : ℕ)) : ℝ) ≥ (R.graph.deg : ℝ) ^ T * (q : ℝ) ^ T / 2 := by + rw [Nat.cast_sub hle] + push_cast + have h2 : (2 : ℝ) * (Fintype.card α * pluralityLoss R.graph.deg q T H : ℕ) + ≤ (R.graph.deg ^ T * q ^ T : ℕ) := by exact_mod_cast hloss + push_cast at h2 + linarith + have hden : (0 : ℝ) < ((Fintype.card α ^ 2 * (R.graph.deg ^ (T + 1) * q ^ T) : ℕ) : ℝ) := by + have : 0 < Fintype.card α ^ 2 * (R.graph.deg ^ (T + 1) * q ^ T) := by + have := R.graph.deg_pos + have := (Fintype.card_pos : 0 < Fintype.card α) + positivity + exact_mod_cast this + have hF : ((R.unsatDarts (R.kDecode q T hq A)).card : ℝ) * (((q : ℝ) - 1) + * ((R.graph.deg : ℝ) ^ T * (q : ℝ) ^ T / 2 * ((R.graph.deg : ℝ) ^ T * (q : ℝ) ^ T / 2))) + / ((Fintype.card α : ℝ) ^ 2 * ((R.graph.deg : ℝ) ^ (T + 1) * (q : ℝ) ^ T)) + ≤ ∑ z : R.graph.V × R.KLabels q T, ((R.goodCrossings q T hq A z).card : ℝ) := by + have h := (div_le_iff₀' hden).2 (R.sum_goodCrossings_ge q T hq A hH hHT hsq) + refine le_trans ?_ h + have hden' : (0 : ℝ) + < (Fintype.card α : ℝ) ^ 2 * ((R.graph.deg : ℝ) ^ (T + 1) * (q : ℝ) ^ T) := by + positivity + have hcastden : ((Fintype.card α ^ 2 * (R.graph.deg ^ (T + 1) * q ^ T) : ℕ) : ℝ) + = (Fintype.card α : ℝ) ^ 2 * ((R.graph.deg : ℝ) ^ (T + 1) * (q : ℝ) ^ T) := by + push_cast; ring + rw [hcastden] + refine div_le_div_of_nonneg_right ?_ hden'.le + push_cast + rw [Nat.cast_sub hq1] + push_cast + refine mul_le_mul_of_nonneg_left ?_ hUpos.le + refine mul_le_mul_of_nonneg_left ?_ (by linarith) + exact mul_le_mul hhalf hhalf (by positivity) (by positivity) + have hB := R.sum_sq_goodCrossings_le q T hq A hlam0 hlam1 hspec hn + obtain ⟨T', rfl⟩ : ∃ T', T = T' + 2 := ⟨T - 2, by omega⟩ + have hP0 : (0 : ℝ) < 2 * ((q : ℝ) ^ (T' + 2) * ((R.graph.deg : ℝ) ^ (T' + 2 - 2) + * (((T' + 2 : ℕ) : ℝ) * ((T' + 2 : ℕ) : ℝ) + * (((R.unsatDarts (R.kDecode q (T' + 2) hq A)).card : ℝ) + * ((R.unsatDarts (R.kDecode q (T' + 2) hq A)).card : ℝ) / (R.graph.order : ℝ)) + + ((T' + 2 : ℕ) : ℝ) * (1 / (1 - lam)) * ((R.graph.deg : ℝ) + * ((R.unsatDarts (R.kDecode q (T' + 2) hq A)).card : ℝ))))) := by + positivity + have hcount := R.card_unsatDarts_ge' q (T' + 2) hq A (by positivity) hF hB hP0 + have hsub : T' + 2 - 2 = T' := by omega + rw [hsub] at hcount + exact powering_algebra T' hN0 hd0 hK0 hq1' hlam1 hUpos hcount + +/-! ### The `min` form -/ + +/-- The slope of the powering bound for small values. -/ +noncomputable def powSlope (c T lam : ℝ) : ℝ := c ^ 2 / (c + 2 + 2 * T / (1 - lam)) + +/-- The floor of the powering bound for large values. -/ +noncomputable def powFloor (c T lam : ℝ) : ℝ := + (c ^ 2 / T ^ 2) / (c + 2 * T ^ 2 + 2 * T / (1 - lam)) + +/-- **The rational bound dominates a `min`.** For values up to `1 / T²` the +bound is linear with slope `powSlope`; beyond that it is at least `powFloor`. -/ +theorem min_le_powBound {c T lam u : ℝ} (hc : 0 ≤ c) (hT : 1 ≤ T) (hlam : lam < 1) + (hu0 : 0 ≤ u) (hu1 : u ≤ 1) : + min (powSlope c T lam * u) (powFloor c T lam) + ≤ c ^ 2 * u / (c + 2 * T ^ 2 * u + 2 * T / (1 - lam)) := by + have hlam' : 0 < 1 - lam := by linarith + have hD : 0 < c + 2 * T ^ 2 * u + 2 * T / (1 - lam) := by positivity + by_cases h : u ≤ 1 / T ^ 2 + · refine le_trans (min_le_left _ _) ?_ + rw [powSlope, div_mul_eq_mul_div] + refine div_le_div_of_nonneg_left (by positivity) hD ?_ + have : 2 * T ^ 2 * u ≤ 2 := by + rw [le_div_iff₀ (by positivity)] at h + linarith + linarith + · refine le_trans (min_le_right _ _) ?_ + rw [powFloor] + push Not at h + have hT2 : 0 < T ^ 2 := by positivity + have hnum : c ^ 2 / T ^ 2 ≤ c ^ 2 * u := by + rw [div_le_iff₀ hT2] + have : 1 / T ^ 2 * T ^ 2 ≤ u * T ^ 2 := + mul_le_mul_of_nonneg_right h.le hT2.le + rw [one_div, inv_mul_cancel₀ hT2.ne'] at this + nlinarith [sq_nonneg c] + have hden : c + 2 * T ^ 2 * u + 2 * T / (1 - lam) ≤ c + 2 * T ^ 2 + 2 * T / (1 - lam) := by + nlinarith + calc (c ^ 2 / T ^ 2) / (c + 2 * T ^ 2 + 2 * T / (1 - lam)) + ≤ (c ^ 2 * u) / (c + 2 * T ^ 2 + 2 * T / (1 - lam)) := + div_le_div_of_nonneg_right hnum (by positivity) + _ ≤ c ^ 2 * u / (c + 2 * T ^ 2 * u + 2 * T / (1 - lam)) := + div_le_div_of_nonneg_left (by positivity) hD hden + +/-- **The powered value, in `min` form.** -/ +theorem le_unsatVal_killedPow_min {H : ℕ} + (hH : 2 * H + 1 < T) (hHT : H + 1 ≤ T) + (hsq : ∀ i ∈ Finset.range (H + 1), ∀ j ∈ Finset.range (H + 1), i + j + 1 < T) + {lam : ℝ} (hlam0 : 0 ≤ lam) (hlam1 : lam < 1) (hspec : R.graph.SpectralBound lam) + (hn : 0 < R.graph.order) (hq1 : 1 ≤ q) + (hloss : 2 * (Fintype.card α * pluralityLoss R.graph.deg q T H) ≤ R.graph.deg ^ T * q ^ T) : + min (powSlope (powConst q α) T lam * ((R.unsatVal : ℚ) : ℝ)) (powFloor (powConst q α) T lam) + ≤ (((R.killedPow q T hq).unsatVal : ℚ) : ℝ) := by + refine R.le_unsatVal_killedPow q T hq fun A => ?_ + have hclean := R.unsatFrac_killedPow_clean q T hq A hH hHT hsq hlam0 hlam1 hspec hn hq1 hloss + have hc0 : 0 ≤ powConst q α := by + rw [powConst] + have : (1 : ℝ) ≤ q := by exact_mod_cast hq1 + apply div_nonneg <;> nlinarith + have hT1 : (1 : ℝ) ≤ T := by + have : 1 ≤ T := by omega + exact_mod_cast this + have hu0 : (0 : ℝ) ≤ ((R.unsatFrac (R.kDecode q T hq A) : ℚ) : ℝ) := by + exact_mod_cast R.unsatFrac_nonneg _ + have hu1 : ((R.unsatFrac (R.kDecode q T hq A) : ℚ) : ℝ) ≤ 1 := by + exact_mod_cast R.unsatFrac_le_one _ + have hv : ((R.unsatVal : ℚ) : ℝ) ≤ ((R.unsatFrac (R.kDecode q T hq A) : ℚ) : ℝ) := by + exact_mod_cast R.unsatVal_le _ + refine le_trans ?_ (le_trans (min_le_powBound hc0 hT1 hlam1 hu0 hu1) hclean) + refine min_le_min_right _ ?_ + have hs : 0 ≤ powSlope (powConst q α) T lam := by + rw [powSlope] + positivity + exact mul_le_mul_of_nonneg_left hv hs + +end RegCSP + +/-! ### Choosing the parameters -/ + +/-- **Bernoulli, for the plurality loss.** With `m = 4 K (q - 1)`, +`4 K (q - 1)^m ≤ q^m`. -/ +theorem four_mul_pow_le {K q : ℕ} (hK : 1 ≤ K) (hq : 2 ≤ q) : + 4 * K * (q - 1) ^ (4 * K * (q - 1)) ≤ q ^ (4 * K * (q - 1)) := by + have hq1 : 1 ≤ q - 1 := by omega + have hqr : ((q - 1 : ℕ) : ℝ) = (q : ℝ) - 1 := by + rw [Nat.cast_sub (by omega)]; push_cast; ring + have hpos : (0 : ℝ) < ((q - 1 : ℕ) : ℝ) := by exact_mod_cast hq1 + set m := 4 * K * (q - 1) with hm + have hbern : (1 : ℝ) + (m : ℝ) * (1 / ((q - 1 : ℕ) : ℝ)) + ≤ (1 + 1 / ((q - 1 : ℕ) : ℝ)) ^ m := + one_add_mul_le_pow (by linarith [one_div_nonneg.2 hpos.le]) m + have hratio : (1 + 1 / ((q - 1 : ℕ) : ℝ)) = (q : ℝ) / ((q - 1 : ℕ) : ℝ) := by + have hne : ((q - 1 : ℕ) : ℝ) ≠ 0 := hpos.ne' + rw [eq_div_iff hne, add_mul, one_mul, one_div, inv_mul_cancel₀ hne, hqr] + ring + rw [hratio, div_pow] at hbern + have hm' : (m : ℝ) * (1 / ((q - 1 : ℕ) : ℝ)) = 4 * K := by + rw [hm] + push_cast + field_simp + rw [hm'] at hbern + have hpm : (0 : ℝ) < ((q - 1 : ℕ) : ℝ) ^ m := by positivity + rw [le_div_iff₀ hpm] at hbern + have : (4 * K : ℝ) * ((q - 1 : ℕ) : ℝ) ^ m ≤ (q : ℝ) ^ m := by + nlinarith + exact_mod_cast this + +/-- The truncation length used for powering: `8 K (q - 1)`. -/ +def powT (K q : ℕ) : ℕ := 2 * (4 * K * (q - 1)) + +/-- The plurality threshold used for powering: `4 K (q - 1) - 1`. -/ +def powH (K q : ℕ) : ℕ := 4 * K * (q - 1) - 1 + +/-- **Parameters for powering.** For `q ≥ 2` and alphabet size `K ≥ 1`, the +choices `powT` and `powH` meet every side condition of the powering bound, for +any degree. -/ +theorem powering_params_spec {K q : ℕ} (hK : 1 ≤ K) (hq : 2 ≤ q) (d : ℕ) : + 2 * powH K q + 1 < powT K q ∧ powH K q + 1 ≤ powT K q + ∧ (∀ i ∈ Finset.range (powH K q + 1), ∀ j ∈ Finset.range (powH K q + 1), + i + j + 1 < powT K q) + ∧ 2 * (K * RegCSP.pluralityLoss d q (powT K q) (powH K q)) + ≤ d ^ powT K q * q ^ powT K q := by + set m := 4 * K * (q - 1) with hm + have hm1 : 1 ≤ m := by + rw [hm] + have : 1 ≤ q - 1 := by omega + nlinarith + have hT : powT K q = 2 * m := rfl + have hH : powH K q = m - 1 := rfl + rw [hT, hH] + refine ⟨by omega, by omega, ?_, ?_⟩ + · intro i hi j hj + rw [Finset.mem_range] at hi hj + omega + · have hHm : m - 1 + 1 = m := by omega + rw [RegCSP.pluralityLoss, hHm] + have hstar := four_mul_pow_le hK hq + rw [← hm] at hstar + have hT' : 2 * m - m = m := by omega + rw [hT'] + have h1 : 4 * K * (q - 1) ^ (2 * m) ≤ q ^ (2 * m) := by + have hpow : (q - 1) ^ (2 * m) = (q - 1) ^ m * (q - 1) ^ m := by + rw [← pow_add]; congr 1; omega + have hpow' : q ^ (2 * m) = q ^ m * q ^ m := by + rw [← pow_add]; congr 1; omega + rw [hpow, hpow'] + have hle : (q - 1) ^ m ≤ q ^ m := Nat.pow_le_pow_left (by omega) m + calc 4 * K * ((q - 1) ^ m * (q - 1) ^ m) = (4 * K * (q - 1) ^ m) * (q - 1) ^ m := by ring + _ ≤ q ^ m * q ^ m := Nat.mul_le_mul hstar hle + have h2 : 4 * K * ((q - 1) ^ m * q ^ m) ≤ q ^ (2 * m) := by + have hpow' : q ^ (2 * m) = q ^ m * q ^ m := by + rw [← pow_add]; congr 1; omega + rw [hpow'] + calc 4 * K * ((q - 1) ^ m * q ^ m) = (4 * K * (q - 1) ^ m) * q ^ m := by ring + _ ≤ q ^ m * q ^ m := Nat.mul_le_mul_right _ hstar + have hsum : 2 * (K * ((q - 1) ^ (2 * m) + (q - 1) ^ m * q ^ m)) ≤ q ^ (2 * m) := by + nlinarith [h1, h2] + calc 2 * (K * (d ^ (2 * m) * (q - 1) ^ (2 * m) + d ^ (2 * m) * ((q - 1) ^ m * q ^ m))) + = d ^ (2 * m) * (2 * (K * ((q - 1) ^ (2 * m) + (q - 1) ^ m * q ^ m))) := by ring + _ ≤ d ^ (2 * m) * q ^ (2 * m) := Nat.mul_le_mul_left _ hsum + +/-- The per-unit slope: `powSlope` grows at least linearly in `q - 1`, with this +coefficient. -/ +noncomputable def slopeUnit (K lam : ℝ) : ℝ := + 1 / (16 * K ^ 4 * (1 / (4 * K ^ 2) + 2 + 16 * K / (1 - lam))) + +/-- **The slope grows linearly in `q`.** -/ +theorem slopeUnit_mul_le_powSlope {K : ℕ} {q : ℕ} (hK : 1 ≤ K) (hq : 2 ≤ q) {lam : ℝ} + (hlam1 : lam < 1) : + slopeUnit (K : ℝ) lam * ((q : ℝ) - 1) + ≤ RegCSP.powSlope (((q : ℝ) - 1) / (4 * (K : ℝ) ^ 2)) (powT K q : ℝ) lam := by + have hq1 : (1 : ℝ) ≤ (q : ℝ) - 1 := by + have : (2 : ℝ) ≤ q := by exact_mod_cast hq + linarith + have hTcast : (powT K q : ℝ) = 8 * K * ((q : ℝ) - 1) := by + rw [powT, Nat.cast_mul, Nat.cast_mul, Nat.cast_mul, Nat.cast_sub (by omega)] + push_cast + ring + rw [hTcast, RegCSP.powSlope, slopeUnit] + set r := (q : ℝ) - 1 with hr + set D₀ : ℝ := 1 / (4 * (K : ℝ) ^ 2) + 2 + 16 * K / (1 - lam) with hD₀ + have hden : r / (4 * (K : ℝ) ^ 2) + 2 + 2 * (8 * K * r) / (1 - lam) ≤ r * D₀ := by + rw [hD₀] + have hx : r / (4 * (K : ℝ) ^ 2) = r * (1 / (4 * (K : ℝ) ^ 2)) := by ring + have hy : 2 * (8 * K * r) / (1 - lam) = r * (16 * K / (1 - lam)) := by ring + rw [hx, hy] + linarith + have hdenpos : 0 < r / (4 * (K : ℝ) ^ 2) + 2 + 2 * (8 * K * r) / (1 - lam) := by positivity + calc 1 / (16 * (K : ℝ) ^ 4 * D₀) * r + = (r / (4 * (K : ℝ) ^ 2)) ^ 2 / (r * D₀) := by + field_simp + ring + _ ≤ (r / (4 * (K : ℝ) ^ 2)) ^ 2 / (r / (4 * (K : ℝ) ^ 2) + 2 + 2 * (8 * K * r) / (1 - lam)) := + div_le_div_of_nonneg_left (by positivity) hdenpos hden + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/Preprocess.lean b/Complexitylib/Classes/PCP/Internal/Preprocess.lean new file mode 100644 index 00000000..74a5507b --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/Preprocess.lean @@ -0,0 +1,196 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.DegreeReductionSound +public import Complexitylib.Classes.PCP.Internal.ExpanderizeCSP +public import Complexitylib.Classes.PCP.Internal.SelfLoops +public import Complexitylib.Classes.PCP.Internal.NumEnc + +/-! +# Preprocessing, assembled + +Dinur's powering step needs its input to be regular, to be an expander, and to +carry a self-loop at every vertex. `preprocess` produces all three from an +arbitrary constraint graph, by composing the three steps already built: + +`preprocess G E = ((G.reduce E).expanderize E).addLoops` + +Each step costs only a constant factor of the value and none of them can turn an +unsatisfiable system satisfiable, so the composite is a gap-preserving reduction +with a constant of its own, `preprocessConst`. + +Degrees compose transparently: degree reduction gives `1 + E.degree`, +expanderizing adds `E.degree`, and the loops add one, for a final degree of +`2 + 2 · E.degree` — a constant, as the amplification bookkeeping requires. The +size is a constant multiple of the original too: the vertex set is fixed by the +last two steps, so it stays the `2 · numEdges` half-edges of the first. + +## Main definitions + +- `ConstraintGraph.preprocess` — the composite +- `ConstraintGraph.preprocessConst` — the constant factor it costs + +## Main results + +- `ConstraintGraph.deg_preprocess`, `order_preprocess` +- `ConstraintGraph.satisfiable_preprocess_of_satisfiable` — completeness +- `ConstraintGraph.le_unsatVal_preprocess` — soundness, with a constant factor +- `ConstraintGraph.spectralBound_preprocess` — the result is an expander, with a + bound strictly below one +-/ + +@[expose] public section + +namespace Complexity + +namespace ConstraintGraph + +variable {α : Type} [DecidableEq α] (G : ConstraintGraph α) (E : ExpanderFamily) + +/-- The darts of a preprocessed system: the self-loop, the edge-link, the +cloud's and the expander's. Naming the type outright — rather than leaving it as +the composite the construction produces — keeps it independent of the graph. -/ +abbrev PreDart (E : ExpanderFamily) : Type := + Unit ⊕ (Option (Fin E.degree) ⊕ Fin E.degree) + +/-- Degree reduction, then expanderizing, then adding self-loops, with the dart +type named. -/ +noncomputable def preprocess : RegCSP α where + graph := + { V := G.HalfEdge + D := PreDart E + decEqV := inferInstance + decEqD := inferInstance + fintypeV := inferInstance + fintypeD := inferInstance + nonemptyD := ⟨Sum.inl ()⟩ + rot := (((G.reduce E).expanderize E).addLoops).graph.rot + rot_involutive := (((G.reduce E).expanderize E).addLoops).graph.rot_involutive } + rel := (((G.reduce E).expanderize E).addLoops).rel + +/-- **It is the composite it is built from.** -/ +theorem preprocess_eq : G.preprocess E = ((G.reduce E).expanderize E).addLoops := rfl + +/-- The preprocessed system's vertices are the half-edges, which are numbered +by their edge and their side. -/ +noncomputable instance : NumEnc (G.preprocess E).graph.V := + inferInstanceAs (NumEnc (Fin G.numEdges × Bool)) + +/-- Its darts are the self-loop, the edge-link, the cloud-links and the +expander's edges, in that order. -/ +noncomputable instance : NumEnc (G.preprocess E).graph.D := + inferInstanceAs (NumEnc (Unit ⊕ (Option (Fin E.degree) ⊕ Fin E.degree))) + +@[simp] theorem order_preprocess : (G.preprocess E).graph.order = 2 * G.numEdges := by + show Fintype.card (Fin G.numEdges × Bool) = _ + simp [Nat.mul_comm] + +@[simp] theorem deg_preprocess : (G.preprocess E).graph.deg = 2 + 2 * E.degree := by + show Fintype.card (PreDart E) = _ + simp + omega + +/-- **Completeness.** -/ +theorem satisfiable_preprocess_of_satisfiable (h : G.Satisfiable) : + (G.preprocess E).Satisfiable := by + rw [preprocess_eq, RegCSP.satisfiable_addLoops_iff, RegCSP.satisfiable_expanderize_iff] + exact G.satisfiable_reduce_of_satisfiable E h + +/-- The constant factor preprocessing costs. -/ +noncomputable def preprocessConst (E : ExpanderFamily) (α : Type) [Fintype α] : ℝ := + reduceConst E α + * (((1 + E.degree : ℕ) : ℝ) / ((1 + E.degree : ℕ) + (E.degree : ℝ))) + * (((1 + 2 * E.degree : ℕ) : ℝ) / (((1 + 2 * E.degree : ℕ) : ℝ) + 1)) + +section Value + +variable [Fintype α] [Nonempty α] + +/-- The value after the two trivial-constraint steps, as a multiple of the value +after degree reduction. -/ +theorem unsatVal_preprocess_eq : + ((G.preprocess E).unsatVal : ℚ) + = (G.reduce E).unsatVal + * ((1 + E.degree : ℕ) : ℚ) / (((1 + E.degree : ℕ) : ℚ) + (E.degree : ℚ)) + * ((1 + 2 * E.degree : ℕ) : ℚ) / ((((1 + 2 * E.degree : ℕ) : ℚ)) + 1) := by + have hdegR : (G.reduce E).graph.deg = 1 + E.degree := by + rw [graph_reduce, deg_reduceGraph] + have hdegX : ((G.reduce E).expanderize E).graph.deg = 1 + 2 * E.degree := by + rw [RegCSP.graph_expanderize, ExpanderFamily.deg_expanderize, hdegR] + ring + rw [preprocess_eq, RegCSP.unsatVal_addLoops, hdegX, RegCSP.unsatVal_expanderize, hdegR] + +/-- **Soundness of preprocessing.** -/ +theorem le_unsatVal_preprocess : + preprocessConst E α * ((G.unsatVal : ℚ) : ℝ) + ≤ (((G.preprocess E).unsatVal : ℚ) : ℝ) := by + set k₁ : ℝ := ((1 + E.degree : ℕ) : ℝ) / (((1 + E.degree : ℕ) : ℝ) + (E.degree : ℝ)) with hk₁ + set k₂ : ℝ := ((1 + 2 * E.degree : ℕ) : ℝ) / (((1 + 2 * E.degree : ℕ) : ℝ) + 1) with hk₂ + have hk₁nn : 0 ≤ k₁ := by rw [hk₁]; positivity + have hk₂nn : 0 ≤ k₂ := by rw [hk₂]; positivity + have hred := G.le_unsatVal_reduce E + have heq : (((G.preprocess E).unsatVal : ℚ) : ℝ) + = (((G.reduce E).unsatVal : ℚ) : ℝ) * k₁ * k₂ := by + rw [G.unsatVal_preprocess_eq E, hk₁, hk₂] + push_cast + ring + rw [heq, preprocessConst, ← hk₁, ← hk₂] + calc reduceConst E α * k₁ * k₂ * ((G.unsatVal : ℚ) : ℝ) + = (reduceConst E α * ((G.unsatVal : ℚ) : ℝ)) * k₁ * k₂ := by ring + _ ≤ (((G.reduce E).unsatVal : ℚ) : ℝ) * k₁ * k₂ := by + exact mul_le_mul_of_nonneg_right + (mul_le_mul_of_nonneg_right hred hk₁nn) hk₂nn + +end Value + +/-! ### The spectral bound -/ + +/-- The spectral bound preprocessing achieves. -/ +noncomputable def preprocessLam (E : ExpanderFamily) : ℝ := + (1 + (1 + 2 * (E.degree : ℝ)) + * (((1 + (E.degree : ℝ)) + (E.degree : ℝ) * E.lam) / ((1 + (E.degree : ℝ)) + E.degree))) + / (1 + (1 + 2 * (E.degree : ℝ))) + +/-- **The preprocessed system is an expander.** -/ +theorem spectralBound_preprocess : + (G.preprocess E).graph.SpectralBound (preprocessLam E) := by + have hdegR : (G.reduce E).graph.deg = 1 + E.degree := by + rw [graph_reduce, deg_reduceGraph] + have hdegX : ((G.reduce E).expanderize E).graph.deg = 1 + 2 * E.degree := by + rw [RegCSP.graph_expanderize, ExpanderFamily.deg_expanderize, hdegR] + ring + have hX := (G.reduce E).spectralBound_expanderize E + rw [hdegR] at hX + set mu : ℝ := (((1 + E.degree : ℕ) : ℝ) + (E.degree : ℝ) * E.lam) + / (((1 + E.degree : ℕ) : ℝ) + (E.degree : ℝ)) with hmu + have hmunn : 0 ≤ mu := by + rw [hmu] + have : (0 : ℝ) ≤ 1 - E.lam := by linarith [E.lam_lt_one] + have hlam : 0 ≤ E.lam := E.lam_nonneg + positivity + have hL := ((G.reduce E).expanderize E).graph.spectralBound_addLoops hmunn hX + rw [hdegX] at hL + have hgoal : preprocessLam E + = (1 + ((1 + 2 * E.degree : ℕ) : ℝ) * mu) / (1 + ((1 + 2 * E.degree : ℕ) : ℝ)) := by + rw [preprocessLam, hmu] + push_cast + ring_nf + rw [hgoal] + exact hL + +theorem preprocessLam_lt_one : preprocessLam E < 1 := by + have hlam := E.lam_lt_one + have hmu : ((1 + (E.degree : ℝ)) + (E.degree : ℝ) * E.lam) + / ((1 + (E.degree : ℝ)) + E.degree) < 1 := by + rw [div_lt_one (by positivity)] + have hdpos : (0 : ℝ) < (E.degree : ℝ) := by exact_mod_cast E.degree_pos + nlinarith + rw [preprocessLam, div_lt_one (by positivity)] + nlinarith [hmu] + +end ConstraintGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/RankCount.lean b/Complexitylib/Classes/PCP/Internal/RankCount.lean new file mode 100644 index 00000000..f561ce47 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/RankCount.lean @@ -0,0 +1,131 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Mathlib.Data.Finset.Sort +public import Mathlib.Data.Fintype.Card +public import Mathlib.Order.Interval.Finset.Fin + +/-! +# Counting below, and the element counted to + +A finite set of numbers is enumerated in increasing order by +`Finset.orderEmbOfFin`. An algorithm cannot enumerate; what it can do is count. +This module connects the two: the position of an element in the increasing +enumeration is the number of elements below it, so an algorithm finds the `k`-th +element by looking for the one with `k` elements below it. + +## Main definitions + +- `Complexity.countBelow` — how many elements of a set lie below a number + +## Main results + +- `Complexity.countBelow_orderEmbOfFin` — counting below the `i`-th element + gives `i` +- `Complexity.orderEmbOfFin_eq_of_countBelow` — so the count names the element +- `Complexity.idxOf_eq_countP` — and in a list sorted by a key, the position of + an entry is the number of entries with a smaller key +-/ + +@[expose] public section + +namespace Complexity + +/-- How many elements of `S` lie below `p`. -/ +def countBelow (S : Finset ℕ) (p : ℕ) : ℕ := (S.filter (· < p)).card + +/-- **Counting below the `i`-th smallest element gives `i`.** -/ +theorem countBelow_orderEmbOfFin (S : Finset ℕ) {k : ℕ} (h : S.card = k) (i : Fin k) : + countBelow S (S.orderEmbOfFin h i) = i.val := by + classical + have himg : Finset.image (S.orderEmbOfFin h) Finset.univ = S := + S.image_orderEmbOfFin_univ h + have hfilter : S.filter (· < S.orderEmbOfFin h i) + = Finset.image (S.orderEmbOfFin h) (Finset.Iio i) := by + ext q + simp only [Finset.mem_filter, Finset.mem_image, Finset.mem_Iio] + constructor + · rintro ⟨hqS, hqlt⟩ + have hq : q ∈ Finset.image (S.orderEmbOfFin h) Finset.univ := by rw [himg]; exact hqS + obtain ⟨j, -, rfl⟩ := Finset.mem_image.mp hq + exact ⟨j, (OrderEmbedding.lt_iff_lt _).mp hqlt, rfl⟩ + · rintro ⟨j, hj, rfl⟩ + exact ⟨S.orderEmbOfFin_mem h j, (OrderEmbedding.lt_iff_lt _).mpr hj⟩ + rw [countBelow, hfilter, Finset.card_image_of_injective _ (S.orderEmbOfFin h).injective, + Fin.card_Iio] + +/-- **The count names the element.** -/ +theorem orderEmbOfFin_eq_of_countBelow {S : Finset ℕ} {k : ℕ} (h : S.card = k) (i : Fin k) + {p : ℕ} (hp : p ∈ S) (hcount : countBelow S p = i.val) : S.orderEmbOfFin h i = p := by + classical + have himg : Finset.image (S.orderEmbOfFin h) Finset.univ = S := + S.image_orderEmbOfFin_univ h + rw [← himg, Finset.mem_image] at hp + obtain ⟨j, -, hj⟩ := hp + have hjcount : countBelow S (S.orderEmbOfFin h j) = j.val := + countBelow_orderEmbOfFin S h j + rw [hj, hcount] at hjcount + rw [← hj] + exact congrArg _ (Fin.ext hjcount) + +/-- **Counting below is strictly monotone along the set.** -/ +theorem countBelow_lt_countBelow {S : Finset ℕ} {j c : ℕ} (hj : j ∈ S) (hlt : j < c) : + countBelow S j < countBelow S c := by + classical + refine Finset.card_lt_card ⟨?_, ?_⟩ + · intro x hx + rw [Finset.mem_filter] at hx ⊢ + exact ⟨hx.1, by omega⟩ + · intro hsub + have hjc : j ∈ S.filter (· < c) := Finset.mem_filter.mpr ⟨hj, hlt⟩ + have := hsub hjc + rw [Finset.mem_filter] at this + omega + +/-- **An element's position is below the size.** -/ +theorem countBelow_lt_card {S : Finset ℕ} {c : ℕ} (hc : c ∈ S) : countBelow S c < S.card := by + classical + refine Finset.card_lt_card ⟨Finset.filter_subset _ _, ?_⟩ + intro hsub + have := hsub hc + rw [Finset.mem_filter] at this + omega + +/-! ### Positions in a sorted list -/ + +/-- **In a list sorted by a key, an entry's position is the number of entries +with a smaller key.** -/ +theorem idxOf_eq_countP {β : Type} [BEq β] [LawfulBEq β] {key : β → ℕ} : + ∀ {l : List β}, List.Pairwise (fun p q => key p ≤ key q) l → + (∀ p ∈ l, ∀ q ∈ l, key p = key q → p = q) → + ∀ {x : β}, x ∈ l → l.idxOf x = l.countP fun q => decide (key q < key x) + | [], _, _, _, hx => by simp at hx + | a :: t, hpair, hinj, x, hx => by + rw [List.pairwise_cons] at hpair + by_cases hxa : x = a + · subst hxa + have hzero : (t.countP fun q => decide (key q < key x)) = 0 := by + refine List.countP_eq_zero.mpr fun q hq => ?_ + have := hpair.1 q hq + simp only [decide_eq_true_eq] + omega + rw [List.idxOf_cons_self, List.countP_cons, hzero] + simp + · have hxt : x ∈ t := by + rcases List.mem_cons.mp hx with h | h + · exact absurd h hxa + · exact h + have hlt : key a < key x := by + have hle := hpair.1 x hxt + have hne : key a ≠ key x := fun h => + hxa (hinj x (List.mem_cons_of_mem _ hxt) a List.mem_cons_self h.symm) + omega + have hih := idxOf_eq_countP hpair.2 + (fun p hp q hq => hinj p (List.mem_cons_of_mem _ hp) q (List.mem_cons_of_mem _ hq)) hxt + rw [List.idxOf_cons_ne _ (Ne.symm hxa), hih, List.countP_cons] + simp only [decide_eq_true_eq, if_pos hlt] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/RegCSP.lean b/Complexitylib/Classes/PCP/Internal/RegCSP.lean new file mode 100644 index 00000000..8ccf4e3a --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/RegCSP.lean @@ -0,0 +1,176 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.RegularGraph +public import Mathlib.Data.Rat.Lemmas +public import Mathlib.Tactic.Positivity + +/-! +# Constraint graphs on a regular graph + +Dinur's argument runs on constraint graphs whose underlying multigraph is +`d`-regular, because that is what makes random walks and the spectral gap +available. This module puts the two halves together: a `RegCSP` is a `RegGraph` +together with a constraint attached to each **dart**, symmetric under reversal, +so that it is really one constraint per undirected edge. + +The measure on constraints is the uniform measure on **darts**: `unsatFrac` +divides the number of unsatisfied darts by `order * deg`. Constraints are +attached to darts rather than to undirected edges, and are *not* required to +agree with their reverse. Nothing is lost: the random walk traverses darts, so +the dart measure is the one every estimate is stated in, and the eventual +verifier samples a dart and checks one constraint. Requiring symmetry instead +would force each construction — powering above all — to prove that reversing a +walk and swapping the two opinions leaves its constraint unchanged, an +index-reversal argument of no mathematical content. + +Two presentations of a constraint system coexist in this development, on +purpose. `ConstraintGraph` has its vertex and edge counts as *numeric fields*, +so it is a single type that a transformation can be iterated on (see +`Amplifier`) and that a bitstring encoding can address; `RegCSP` carries +structured vertex and dart types, which is what Dinur's constructions produce. +The bijective bridge between them belongs to the final encoded reduction and is +built there. + +## Main definitions + +- `RegCSP`, `RegCSP.Dart`, `RegCSP.Assignment`, `RegCSP.satisfies`, + `RegCSP.Satisfies`, `RegCSP.Satisfiable`, `RegCSP.unsatDarts` +- `RegCSP.unsatFrac`, `RegCSP.unsatVal` — the fraction of darts an assignment + fails, and its minimum over assignments + +## Main results + +- `RegCSP.unsatFrac_eq_zero_iff`, `RegCSP.unsatVal_eq_zero_iff_satisfiable` — + value zero is satisfiability +- `RegCSP.unsatVal_nonneg`, `RegCSP.unsatVal_le_one` +-/ + +@[expose] public section + +namespace Complexity + +/-- A constraint system on a regular multigraph: one constraint per **dart**. -/ +structure RegCSP (α : Type) where + /-- The underlying regular multigraph. -/ + graph : RegGraph + /-- The constraint on the dart `(v, i)`, as a predicate on the label of `v` + and the label of its `i`-th neighbour, in that order. -/ + rel : graph.V → graph.D → α → α → Bool + +namespace RegCSP + +variable {α : Type} (R : RegCSP α) + +/-- A dart: a vertex together with one of its `d` outgoing edge labels. -/ +abbrev Dart (R : RegCSP α) : Type := R.graph.V × R.graph.D + +/-- An assignment labels every vertex with a symbol of the alphabet. -/ +abbrev Assignment (R : RegCSP α) : Type := R.graph.V → α + +/-- Whether the dart `p`'s constraint holds under `a`, as a `Bool`. -/ +def satisfies (a : R.Assignment) (p : R.Dart) : Bool := + R.rel p.1 p.2 (a p.1) (a (R.graph.nbr p.1 p.2)) + +/-- The dart `p`'s constraint holds under `a`. -/ +def Satisfies (a : R.Assignment) (p : R.Dart) : Prop := R.satisfies a p = true + +instance (a : R.Assignment) (p : R.Dart) : Decidable (R.Satisfies a p) := + inferInstanceAs (Decidable (R.satisfies a p = true)) + +/-- The darts left unsatisfied by `a`. -/ +def unsatDarts (a : R.Assignment) : Finset R.Dart := + Finset.univ.filter fun p => ¬ R.Satisfies a p + +@[simp] theorem mem_unsatDarts {a : R.Assignment} {p : R.Dart} : + p ∈ R.unsatDarts a ↔ ¬ R.Satisfies a p := by + simp [unsatDarts] + +/-- Some assignment satisfies every dart. -/ +def Satisfiable : Prop := ∃ a : R.Assignment, ∀ p, R.Satisfies a p + +/-- The number of darts. -/ +theorem card_dart : Fintype.card R.Dart = R.graph.order * R.graph.deg := by + simp + +theorem card_unsatDarts_le (a : R.Assignment) : + (R.unsatDarts a).card ≤ R.graph.order * R.graph.deg := by + have h := Finset.card_le_univ (R.unsatDarts a) + rwa [card_dart] at h + +/-- The fraction of darts an assignment leaves unsatisfied. -/ +def unsatFrac (a : R.Assignment) : ℚ := + ((R.unsatDarts a).card : ℚ) / ((R.graph.order * R.graph.deg : ℕ) : ℚ) + +theorem unsatFrac_nonneg (a : R.Assignment) : 0 ≤ R.unsatFrac a := by + unfold unsatFrac; positivity + +theorem unsatFrac_le_one (a : R.Assignment) : R.unsatFrac a ≤ 1 := by + rcases Nat.eq_zero_or_pos (R.graph.order * R.graph.deg) with h | h + · simp [unsatFrac, h] + · have hpos : (0 : ℚ) < ((R.graph.order * R.graph.deg : ℕ) : ℚ) := by exact_mod_cast h + rw [unsatFrac, div_le_one hpos] + exact_mod_cast R.card_unsatDarts_le a + +/-- An assignment wastes no darts exactly when it satisfies them all. -/ +theorem unsatFrac_eq_zero_iff {a : R.Assignment} : + R.unsatFrac a = 0 ↔ ∀ p, R.Satisfies a p := by + constructor + · intro h p + by_contra hp + have hne : (R.unsatDarts a).Nonempty := ⟨p, by simpa using hp⟩ + have hcard : 0 < (R.unsatDarts a).card := Finset.card_pos.mpr hne + have hm : 0 < R.graph.order * R.graph.deg := lt_of_lt_of_le hcard (R.card_unsatDarts_le a) + have hmq : (0 : ℚ) < ((R.graph.order * R.graph.deg : ℕ) : ℚ) := by exact_mod_cast hm + have hcq : (0 : ℚ) < ((R.unsatDarts a).card : ℚ) := by exact_mod_cast hcard + rw [unsatFrac, div_eq_zero_iff] at h + rcases h with h | h + · exact absurd h (ne_of_gt hcq) + · exact absurd h (ne_of_gt hmq) + · intro h + have hempty : R.unsatDarts a = ∅ := by + ext p; simpa using h p + simp [unsatFrac, hempty] + +section Value + +variable [Fintype α] [Nonempty α] + +/-- The least fraction of darts any assignment leaves unsatisfied. -/ +noncomputable def unsatVal : ℚ := + (Finset.univ : Finset R.Assignment).inf' Finset.univ_nonempty R.unsatFrac + +theorem unsatVal_le (a : R.Assignment) : R.unsatVal ≤ R.unsatFrac a := + Finset.inf'_le _ (Finset.mem_univ a) + +theorem le_unsatVal {c : ℚ} (h : ∀ a : R.Assignment, c ≤ R.unsatFrac a) : c ≤ R.unsatVal := + Finset.le_inf' _ _ fun a _ => h a + +theorem exists_assignment_unsatFrac_eq_unsatVal : + ∃ a : R.Assignment, R.unsatFrac a = R.unsatVal := by + obtain ⟨a, -, ha⟩ := Finset.exists_mem_eq_inf' (Finset.univ_nonempty) R.unsatFrac + exact ⟨a, ha.symm⟩ + +theorem unsatVal_nonneg : 0 ≤ R.unsatVal := R.le_unsatVal fun a => R.unsatFrac_nonneg a + +theorem unsatVal_le_one : R.unsatVal ≤ 1 := by + obtain ⟨a, ha⟩ := R.exists_assignment_unsatFrac_eq_unsatVal + exact ha ▸ R.unsatFrac_le_one a + +theorem unsatVal_eq_zero_iff_satisfiable : R.unsatVal = 0 ↔ R.Satisfiable := by + constructor + · intro h + obtain ⟨a, ha⟩ := R.exists_assignment_unsatFrac_eq_unsatVal + exact ⟨a, R.unsatFrac_eq_zero_iff.mp (ha.trans h)⟩ + · rintro ⟨a, ha⟩ + have h0 : R.unsatFrac a = 0 := R.unsatFrac_eq_zero_iff.mpr ha + exact le_antisymm (h0 ▸ R.unsatVal_le a) R.unsatVal_nonneg + +end Value + +end RegCSP + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/RegularGraph.lean b/Complexitylib/Classes/PCP/Internal/RegularGraph.lean new file mode 100644 index 00000000..22719b37 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/RegularGraph.lean @@ -0,0 +1,249 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Mathlib.Algebra.BigOperators.Field +public import Mathlib.Algebra.BigOperators.Fin +public import Mathlib.Algebra.Order.BigOperators.Ring.Finset +public import Mathlib.Algebra.Order.Chebyshev +public import Mathlib.Data.Fintype.BigOperators +public import Mathlib.Data.Fintype.Prod +public import Mathlib.Data.Real.Basic +public import Mathlib.Tactic.FieldSimp +public import Mathlib.Tactic.Linarith +public import Mathlib.Tactic.Positivity +public import Mathlib.Tactic.Ring + +/-! +# Regular graphs, their walk operator, and the spectral gap + +The graph-theoretic substrate of Dinur's proof: regular multigraphs given by a +**rotation map**, the associated random-walk averaging operator, and a +square-norm formulation of the spectral gap. + +A `RegGraph` carries a finite vertex type `V`, a finite nonempty label type `D`, +and an involution `rot : V × D → V × D` on *darts* (a vertex together with one +of its outgoing edge labels). The involution pairs each dart with its reverse, +which is what makes the graph undirected and `|D|`-regular, with parallel edges +and self-loops allowed — all three are needed, since powering and +expanderization produce them. The neighbour function is `nbr v i = (rot (v,i)).1`. + +## Why `V` and `D` are types, not numbers + +Dinur's constructions build new graphs whose vertices and labels are *structured*: +powering takes the label type to walk tuples `Fin t → D`, and degree reduction +takes the vertex type to the dart type `V × D`. Carrying `V` and `D` as types +lets those constructions be written directly, with no encoding bijections; the +translation to `Fin`-indexed data is deferred to the one place that needs it, +the encoded reduction at the very end. + +## The spectral gap, without square roots + +Rather than second eigenvalues, `SpectralBound G lam` says directly that the +walk operator contracts *mean-zero* functions by `lam` in the Euclidean norm, +stated on **squared** norms: + +`∑ v, (step f v) ^ 2 ≤ lam ^ 2 * ∑ v, (f v) ^ 2` whenever `∑ v, f v = 0`. + +This avoids `Real.sqrt` and eigenvalue machinery entirely, and it is exactly +the form the walk analysis needs: `step` preserves sums (so it preserves +mean-zero-ness), hence the bound self-composes and `t` steps contract by +`lam ^ t`. + +## Main definitions + +- `RegGraph`, `RegGraph.deg`, `RegGraph.order`, `RegGraph.nbr`, + `RegGraph.step`, `RegGraph.stepIter` +- `RegGraph.SpectralBound` + +## Main results + +- `RegGraph.sum_nbr` — summing over darts is summing over vertices, `deg` times +- `RegGraph.sum_step` — the walk operator preserves sums +- `RegGraph.sum_sq_step_le` — it is a contraction, with no spectral hypothesis +- `RegGraph.sum_sq_stepIter_le` — `t` steps contract mean-zero functions by + `lam ^ (2 * t)` in squared norm +-/ + +@[expose] public section + +namespace Complexity + +/-- A regular multigraph, presented by a rotation map: an involution on darts +`(vertex, edge label)` sending each dart to its reverse. Every vertex has +exactly one dart per label, so the graph is `|D|`-regular; parallel edges and +self-loops are allowed. -/ +structure RegGraph where + /-- The vertex type. -/ + V : Type + /-- The edge-label type; each vertex has one outgoing dart per label. -/ + D : Type + /-- Vertices have decidable equality, so assignments form a `Fintype`. -/ + decEqV : DecidableEq V + /-- Labels have decidable equality, so label tuples form a `Fintype`. -/ + decEqD : DecidableEq D + /-- The vertex type is finite. -/ + fintypeV : Fintype V + /-- The label type is finite. -/ + fintypeD : Fintype D + /-- The label type is nonempty, i.e. the degree is positive. -/ + nonemptyD : Nonempty D + /-- The rotation map, sending a dart to its reverse. -/ + rot : V × D → V × D + /-- Reversing a dart twice is the identity. -/ + rot_involutive : Function.Involutive rot + +attribute [instance] RegGraph.decEqV RegGraph.decEqD RegGraph.fintypeV RegGraph.fintypeD + RegGraph.nonemptyD + +namespace RegGraph + +variable (G : RegGraph) + +/-- The degree: the number of darts at each vertex. -/ +def deg : ℕ := Fintype.card G.D + +/-- The number of vertices. -/ +def order : ℕ := Fintype.card G.V + +theorem deg_pos : 0 < G.deg := Fintype.card_pos + +@[simp] theorem card_eq_order : Fintype.card G.V = G.order := rfl + +@[simp] theorem card_eq_deg : Fintype.card G.D = G.deg := rfl + +theorem deg_ne_zero : (G.deg : ℝ) ≠ 0 := Nat.cast_ne_zero.mpr G.deg_pos.ne' + +/-- The `i`-th neighbour of `v`. -/ +def nbr (v : G.V) (i : G.D) : G.V := (G.rot (v, i)).1 + +/-- The rotation map is a bijection of darts. -/ +theorem rot_bijective : Function.Bijective G.rot := G.rot_involutive.bijective + +/-- Summing a function of the neighbour over all darts is summing it over all +vertices, each counted `deg` times. This is the rotation-map form of +regularity, and it is the source of every counting identity below. Stated for an +arbitrary `AddCommMonoid`, since the walk analysis needs it both for real +averages and for counting in `ℕ`. -/ +theorem sum_nbr_nsmul {M : Type*} [AddCommMonoid M] (f : G.V → M) : + ∑ v : G.V, ∑ i : G.D, f (G.nbr v i) = G.deg • ∑ v : G.V, f v := by + have hprod : ∑ p : G.V × G.D, f (G.rot p).1 = ∑ p : G.V × G.D, f p.1 := + Fintype.sum_bijective G.rot G.rot_bijective _ _ fun _ => rfl + calc ∑ v : G.V, ∑ i : G.D, f (G.nbr v i) + = ∑ p : G.V × G.D, f (G.rot p).1 := + (Fintype.sum_prod_type (fun p : G.V × G.D => f (G.rot p).1)).symm + _ = ∑ p : G.V × G.D, f p.1 := hprod + _ = ∑ v : G.V, ∑ _i : G.D, f v := Fintype.sum_prod_type (fun p : G.V × G.D => f p.1) + _ = ∑ v : G.V, G.deg • f v := by simp + _ = G.deg • ∑ v : G.V, f v := Finset.sum_nsmul _ _ _ + +theorem sum_nbr (f : G.V → ℝ) : + ∑ v : G.V, ∑ i : G.D, f (G.nbr v i) = (G.deg : ℝ) * ∑ v : G.V, f v := by + rw [G.sum_nbr_nsmul f, nsmul_eq_mul] + +/-- One step of the random walk, as an averaging operator on real-valued +functions on the vertices. -/ +noncomputable def step (f : G.V → ℝ) (v : G.V) : ℝ := + (∑ i : G.D, f (G.nbr v i)) / (G.deg : ℝ) + +/-- The walk operator preserves sums: it is doubly stochastic. -/ +theorem sum_step (f : G.V → ℝ) : ∑ v : G.V, G.step f v = ∑ v : G.V, f v := by + have hd : (G.deg : ℝ) ≠ 0 := G.deg_ne_zero + calc ∑ v : G.V, G.step f v + = (∑ v : G.V, ∑ i : G.D, f (G.nbr v i)) / (G.deg : ℝ) := by + rw [Finset.sum_div]; rfl + _ = ((G.deg : ℝ) * ∑ v : G.V, f v) / (G.deg : ℝ) := by rw [G.sum_nbr f] + _ = ∑ v : G.V, f v := by field_simp + +/-- The `t`-step walk operator. -/ +noncomputable def stepIter (t : ℕ) (f : G.V → ℝ) : G.V → ℝ := G.step^[t] f + +@[simp] theorem stepIter_zero (f : G.V → ℝ) : G.stepIter 0 f = f := rfl + +theorem stepIter_succ (t : ℕ) (f : G.V → ℝ) : + G.stepIter (t + 1) f = G.step (G.stepIter t f) := + Function.iterate_succ_apply' _ _ _ + +theorem sum_stepIter (t : ℕ) (f : G.V → ℝ) : + ∑ v : G.V, G.stepIter t f v = ∑ v : G.V, f v := by + induction t with + | zero => simp + | succ t ih => rw [stepIter_succ, G.sum_step, ih] + +/-- **The walk operator is a contraction.** Averaging can only shrink the +Euclidean norm — this is Cauchy–Schwarz on each vertex's average, summed with +`sum_nbr`. It holds for *every* regular graph, with no spectral assumption, and +is what lets a graph be combined with an expander: the graph's own part of the +combined walk contributes at most its full weight. -/ +theorem sum_sq_step_le (f : G.V → ℝ) : + (∑ v : G.V, (G.step f v) ^ 2) ≤ ∑ v : G.V, (f v) ^ 2 := by + have hd : (0 : ℝ) < (G.deg : ℝ) := by + have := G.deg_pos + positivity + have hpt : ∀ v : G.V, (G.step f v) ^ 2 ≤ (∑ i : G.D, (f (G.nbr v i)) ^ 2) / (G.deg : ℝ) := by + intro v + have hcs : (∑ i : G.D, f (G.nbr v i)) ^ 2 + ≤ (G.deg : ℝ) * ∑ i : G.D, (f (G.nbr v i)) ^ 2 := by + have h := sq_sum_le_card_mul_sum_sq (s := (Finset.univ : Finset G.D)) + (f := fun i => f (G.nbr v i)) + rwa [Finset.card_univ, card_eq_deg] at h + rw [step, div_pow, div_le_div_iff₀ (by positivity) hd] + calc (∑ i : G.D, f (G.nbr v i)) ^ 2 * (G.deg : ℝ) + ≤ ((G.deg : ℝ) * ∑ i : G.D, (f (G.nbr v i)) ^ 2) * (G.deg : ℝ) := by + exact mul_le_mul_of_nonneg_right hcs (le_of_lt hd) + _ = (∑ i : G.D, (f (G.nbr v i)) ^ 2) * (G.deg : ℝ) ^ 2 := by ring + calc ∑ v : G.V, (G.step f v) ^ 2 + ≤ ∑ v : G.V, (∑ i : G.D, (f (G.nbr v i)) ^ 2) / (G.deg : ℝ) := + Finset.sum_le_sum fun v _ => hpt v + _ = (∑ v : G.V, ∑ i : G.D, (f (G.nbr v i)) ^ 2) / (G.deg : ℝ) := by + rw [Finset.sum_div] + _ = ((G.deg : ℝ) * ∑ v : G.V, (f v) ^ 2) / (G.deg : ℝ) := by + rw [G.sum_nbr (fun v => (f v) ^ 2)] + _ = ∑ v : G.V, (f v) ^ 2 := by field_simp + +/-- `G` has spectral gap at least `1 - lam`: the walk operator contracts every +mean-zero function by a factor `lam` in the Euclidean norm, stated on squared +norms so that no square roots are needed. -/ +def SpectralBound (G : RegGraph) (lam : ℝ) : Prop := + ∀ f : G.V → ℝ, (∑ v : G.V, f v) = 0 → + (∑ v : G.V, (G.step f v) ^ 2) ≤ lam ^ 2 * ∑ v : G.V, (f v) ^ 2 + +/-- A weaker contraction factor is still a contraction factor. -/ +theorem SpectralBound.mono {G : RegGraph} {lam lam' : ℝ} (h : G.SpectralBound lam) + (h0 : 0 ≤ lam) (hle : lam ≤ lam') : G.SpectralBound lam' := by + intro f hf + refine le_trans (h f hf) (mul_le_mul_of_nonneg_right ?_ (by positivity)) + exact pow_le_pow_left₀ h0 hle 2 + +/-- Every graph contracts the empty family of mean-zero functions: a graph with +no vertices has any contraction factor. -/ +theorem spectralBound_of_isEmpty {G : RegGraph} (h : IsEmpty G.V) (lam : ℝ) : + G.SpectralBound lam := by + intro f _ + have he : (Finset.univ : Finset G.V) = ∅ := Finset.univ_eq_empty + rw [he, Finset.sum_empty, Finset.sum_empty, mul_zero] + +/-- The contraction self-composes: `t` steps contract a mean-zero function by +`lam ^ t`, i.e. `lam ^ (2 * t)` in squared norm. -/ +theorem sum_sq_stepIter_le {lam : ℝ} (h : G.SpectralBound lam) (t : ℕ) (f : G.V → ℝ) + (hf : (∑ v : G.V, f v) = 0) : + (∑ v : G.V, (G.stepIter t f v) ^ 2) ≤ lam ^ (2 * t) * ∑ v : G.V, (f v) ^ 2 := by + induction t with + | zero => simp + | succ t ih => + have hzero : (∑ v : G.V, G.stepIter t f v) = 0 := by rw [G.sum_stepIter, hf] + have hstep := h (G.stepIter t f) hzero + have hpow : (0 : ℝ) ≤ lam ^ 2 := by positivity + calc ∑ v : G.V, (G.stepIter (t + 1) f v) ^ 2 + = ∑ v : G.V, (G.step (G.stepIter t f) v) ^ 2 := by rw [stepIter_succ] + _ ≤ lam ^ 2 * ∑ v : G.V, (G.stepIter t f v) ^ 2 := hstep + _ ≤ lam ^ 2 * (lam ^ (2 * t) * ∑ v : G.V, (f v) ^ 2) := + mul_le_mul_of_nonneg_left ih hpow + _ = lam ^ (2 * (t + 1)) * ∑ v : G.V, (f v) ^ 2 := by + rw [← mul_assoc, ← pow_add]; ring_nf + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/SecondMoment.lean b/Complexitylib/Classes/PCP/Internal/SecondMoment.lean new file mode 100644 index 00000000..dc33fb5a --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/SecondMoment.lean @@ -0,0 +1,87 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Mathlib.Algebra.Order.BigOperators.Ring.Finset +public import Mathlib.Data.Fintype.BigOperators +public import Mathlib.Data.Real.Basic +public import Mathlib.Tactic.Positivity +public import Mathlib.Tactic.Linarith + +/-! +# A second-moment support bound + +The Paley–Zygmund style inequality behind Dinur's powering estimate: a +nonnegative random variable is nonzero on a set at least as large as the square +of its mean over its second moment. Written with sums rather than expectations, +so that the normalising cardinality cancels on its own: + +`(∑ N) ^ 2 ≤ |support N| · ∑ N ^ 2` + +In the powering analysis `N` counts how many *faulty* edges a random walk +traverses. The first moment is proportional to the gap, the second moment is +controlled by the expander mixing lemma, and this inequality converts the two +into a lower bound on the probability that the walk meets a faulty edge at all +— which is what the powered constraint detects. + +## Main results + +- `sq_sum_le_card_support_mul_sum_sq` — the inequality above +- `card_ge_of_moments` — Paley–Zygmund in counting form +-/ + +@[expose] public section + +namespace Complexity + +open Classical in +/-- **Second-moment support bound.** The square of a sum is at most the size of +the summand's support times the sum of squares. -/ +theorem sq_sum_le_card_support_mul_sum_sq {ι : Type*} [Fintype ι] (N : ι → ℝ) : + (∑ i, N i) ^ 2 ≤ ((Finset.univ.filter fun i => N i ≠ 0).card : ℝ) * ∑ i, (N i) ^ 2 := by + classical + have hcross : ∑ i, (if N i ≠ 0 then (1 : ℝ) else 0) * N i = ∑ i, N i := by + refine Finset.sum_congr rfl fun i _ => ?_ + by_cases h : N i = 0 <;> simp [h] + have hfsq : ∑ i, (if N i ≠ 0 then (1 : ℝ) else 0) ^ 2 + = ((Finset.univ.filter fun i => N i ≠ 0).card : ℝ) := by + rw [Finset.card_filter] + push_cast + refine Finset.sum_congr rfl fun i _ => ?_ + by_cases h : N i = 0 <;> simp [h] + have hcs := Finset.sum_mul_sq_le_sq_mul_sq Finset.univ + (fun i => if N i ≠ 0 then (1 : ℝ) else 0) N + rwa [hcross, hfsq] at hcs + +open Classical in +/-- **Paley–Zygmund, in counting form.** If a nonnegative count has first moment +at least `A` and second moment at most `B`, then at least `A ^ 2 / B` of the +indices carry a nonzero count. Any `S` containing the support inherits the +bound — in the powering argument `S` is the set of unsatisfied constraints and +the count is the number of crossings that break one. -/ +theorem card_ge_of_moments {ι : Type*} [Fintype ι] (N : ι → ℝ) (S : Finset ι) + (hsupp : ∀ i, N i ≠ 0 → i ∈ S) {A B : ℝ} (hA0 : 0 ≤ A) (hA : A ≤ ∑ i, N i) + (hB : ∑ i, (N i) ^ 2 ≤ B) (hB0 : 0 < B) : + A ^ 2 / B ≤ (S.card : ℝ) := by + classical + have h1 := sq_sum_le_card_support_mul_sum_sq N + have h2 : ((Finset.univ.filter fun i => N i ≠ 0).card : ℝ) ≤ (S.card : ℝ) := by + have hsub : (Finset.univ.filter fun i => N i ≠ 0) ⊆ S := by + intro i hi + simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hi + exact hsupp i hi + exact_mod_cast Finset.card_le_card hsub + have h3 : A ^ 2 ≤ (∑ i, N i) ^ 2 := by nlinarith [hA, hA0] + have h4 : (0 : ℝ) ≤ ∑ i, (N i) ^ 2 := Finset.sum_nonneg fun i _ => sq_nonneg _ + have h5 : A ^ 2 ≤ (S.card : ℝ) * B := by + calc A ^ 2 ≤ (∑ i, N i) ^ 2 := h3 + _ ≤ ((Finset.univ.filter fun i => N i ≠ 0).card : ℝ) * ∑ i, (N i) ^ 2 := h1 + _ ≤ (S.card : ℝ) * B := by + refine mul_le_mul h2 hB h4 ?_ + positivity + rw [div_le_iff₀ hB0] + exact h5 + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/SelfLoops.lean b/Complexitylib/Classes/PCP/Internal/SelfLoops.lean new file mode 100644 index 00000000..7a36f5c9 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/SelfLoops.lean @@ -0,0 +1,185 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.RegCSP +public import Complexitylib.Classes.PCP.Internal.Union +public import Complexitylib.Classes.PCP.Internal.WalkPath + +/-! +# Adding self-loops + +Powering names a vertex within distance `k ≤ h` of `v` by a length-`h` walk that +takes `k` real steps and then stays put, so the graph it runs on must have a +self-loop at every vertex (`RegGraph.Loops`). This module adds them. + +A self-loop at every vertex is itself a `RegGraph` — `loopGraph`, of degree one, +whose rotation map is the identity — so adding loops is just `union` with it, +and the spectral bound comes free from `spectralBound_union`: the loops +contribute their full weight `1` and the original graph its `lam`, giving +`(1 + deg · lam) / (1 + deg)`, still below one. + +On the constraint side the loops carry the trivially true constraint. They are +never violated, so the number of broken darts is unchanged while the number of +darts grows by a factor `(deg + 1) / deg`; the value is scaled by exactly +`deg / (deg + 1)`, which `unsatFrac_addLoops` records. + +## Main definitions + +- `RegGraph.loopGraph` — one self-loop at every vertex +- `RegCSP.addLoops` — the constraint system with loops + +## Main results + +- `RegGraph.deg_addLoops`, `RegGraph.spectralBound_addLoops` +- `RegCSP.unsatFrac_addLoops`, `unsatVal_addLoops` — the value scales by + `deg / (deg + 1)` +- `RegCSP.satisfiable_addLoops_iff` +-/ + +@[expose] public section + +namespace Complexity + +namespace RegGraph + +/-- The graph with exactly one self-loop at each vertex. -/ +def loopGraph (V : Type) [DecidableEq V] [Fintype V] : RegGraph where + V := V + D := Unit + decEqV := inferInstance + decEqD := inferInstance + fintypeV := inferInstance + fintypeD := inferInstance + nonemptyD := ⟨()⟩ + rot p := p + rot_involutive _ := rfl + +@[simp] theorem V_loopGraph (V : Type) [DecidableEq V] [Fintype V] : + (loopGraph V).V = V := rfl + +@[simp] theorem deg_loopGraph (V : Type) [DecidableEq V] [Fintype V] : + (loopGraph V).deg = 1 := Fintype.card_unit + +variable (G : RegGraph) + +/-- `G` with a self-loop added at every vertex. -/ +def addLoops : RegGraph := union (loopGraph G.V) G (Equiv.refl G.V) + +@[simp] theorem V_addLoops : G.addLoops.V = G.V := rfl + +@[simp] theorem order_addLoops : G.addLoops.order = G.order := rfl + +@[simp] theorem deg_addLoops : G.addLoops.deg = 1 + G.deg := by + rw [addLoops, deg_union, deg_loopGraph] + +/-- Adding loops keeps a spectral bound below one. -/ +theorem spectralBound_addLoops {lam : ℝ} (hlam : 0 ≤ lam) (h : G.SpectralBound lam) : + G.addLoops.SpectralBound ((1 + (G.deg : ℝ) * lam) / (1 + (G.deg : ℝ))) := by + have hu := spectralBound_union (loopGraph G.V) G (Equiv.refl G.V) hlam h + rw [deg_loopGraph] at hu + simpa using hu + +end RegGraph + +namespace RegCSP + +variable {α : Type} (R : RegCSP α) + +/-- `R` with a trivially satisfied self-loop added at every vertex. -/ +def addLoops : RegCSP α where + graph := R.graph.addLoops + rel v d a b := + match d with + | Sum.inl _ => true + | Sum.inr i => R.rel v i a b + +@[simp] theorem graph_addLoops : R.addLoops.graph = R.graph.addLoops := rfl + +/-- The broken darts are unchanged: only the original constraints can fail. -/ +theorem card_unsatDarts_addLoops (a : R.Assignment) : + (R.addLoops.unsatDarts a).card = (R.unsatDarts a).card := by + classical + refine (Finset.card_bij (fun q _ => ((q.1, Sum.inr q.2) : R.addLoops.Dart)) ?_ ?_ ?_).symm + · intro q hq + rw [mem_unsatDarts] at hq ⊢ + intro hcon + exact hq hcon + · intro q _ q' _ heq + have h1 : q.1 = q'.1 := congrArg (fun r => (r.1 : R.graph.V)) heq + have h2 : Sum.inr q.2 = (Sum.inr q'.2 : Unit ⊕ R.graph.D) := + congrArg (fun r => (r.2 : Unit ⊕ R.graph.D)) heq + exact Prod.ext h1 (Sum.inr.inj h2) + · rintro ⟨v, _ | i⟩ hq + · exfalso + rw [mem_unsatDarts] at hq + exact hq rfl + · refine ⟨(v, i), ?_, rfl⟩ + rw [mem_unsatDarts] at hq ⊢ + intro hcon + exact hq hcon + +/-- Adding loops scales the value by `deg / (deg + 1)`. -/ +theorem unsatFrac_addLoops (a : R.Assignment) : + R.addLoops.unsatFrac a + = R.unsatFrac a * (R.graph.deg : ℚ) / ((R.graph.deg : ℚ) + 1) := by + have hd : (0 : ℚ) < (R.graph.deg : ℚ) := by + have := R.graph.deg_pos + exact_mod_cast this + have hcards := R.card_unsatDarts_addLoops a + rcases Nat.eq_zero_or_pos R.graph.order with hz | hz + · -- no vertices: no darts at all + have hempty : (R.unsatDarts a).card = 0 := by + have hle : (R.unsatDarts a).card ≤ R.graph.order * R.graph.deg := + R.card_unsatDarts_le a + rw [hz] at hle + omega + have hempty' : (R.addLoops.unsatDarts a).card = 0 := by rw [hcards, hempty] + rw [unsatFrac, unsatFrac, hempty, hempty'] + simp + · have hzq : (0 : ℚ) < (R.graph.order : ℚ) := by exact_mod_cast hz + rw [unsatFrac, unsatFrac, hcards] + have hden : ((R.addLoops.graph.order * R.addLoops.graph.deg : ℕ) : ℚ) + = (R.graph.order : ℚ) * ((R.graph.deg : ℚ) + 1) := by + rw [graph_addLoops, RegGraph.order_addLoops, RegGraph.deg_addLoops] + push_cast + ring + rw [hden] + field_simp + push_cast + ring + +/-- The scaling passes to the value. -/ +theorem unsatVal_addLoops [Fintype α] [Nonempty α] : + R.addLoops.unsatVal = R.unsatVal * (R.graph.deg : ℚ) / ((R.graph.deg : ℚ) + 1) := by + have hk : (0 : ℚ) ≤ (R.graph.deg : ℚ) / ((R.graph.deg : ℚ) + 1) := by positivity + obtain ⟨a, ha⟩ := R.exists_assignment_unsatFrac_eq_unsatVal + obtain ⟨b, hb⟩ := R.addLoops.exists_assignment_unsatFrac_eq_unsatVal + refine le_antisymm ?_ ?_ + · calc R.addLoops.unsatVal ≤ R.addLoops.unsatFrac a := R.addLoops.unsatVal_le a + _ = R.unsatFrac a * (R.graph.deg : ℚ) / ((R.graph.deg : ℚ) + 1) := R.unsatFrac_addLoops a + _ = R.unsatVal * (R.graph.deg : ℚ) / ((R.graph.deg : ℚ) + 1) := by rw [ha] + · rw [← hb, R.unsatFrac_addLoops b, mul_div_assoc, mul_div_assoc] + exact mul_le_mul_of_nonneg_right (R.unsatVal_le b) hk + +theorem satisfiable_addLoops_iff : R.addLoops.Satisfiable ↔ R.Satisfiable := by + constructor + · rintro ⟨a, ha⟩ + refine ⟨a, fun p => ?_⟩ + have h := ha (p.1, Sum.inr p.2) + rw [Satisfies, satisfies] at h ⊢ + exact h + · rintro ⟨a, ha⟩ + refine ⟨a, ?_⟩ + rintro ⟨v, _ | i⟩ + · rw [Satisfies, satisfies] + rfl + · have h := ha (v, i) + rw [Satisfies, satisfies] at h ⊢ + exact h + +end RegCSP + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/SquareVerifier.lean b/Complexitylib/Classes/PCP/Internal/SquareVerifier.lean new file mode 100644 index 00000000..67a65c18 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/SquareVerifier.lean @@ -0,0 +1,349 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.Containments +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.PCP.Internal.PosScan +public import Complexitylib.Classes.EventProb + +/-! +# Running a verifier twice + +Dinur's amplification leaves a constant gap, which need not be as large as the +one half the `PCP` classes ask for. Independent repetition closes that: two runs +on independent coins reject a non-member with probability `1 - (1 - s)²`, and +repeating the doubling a constant number of times drives the error below any +threshold. + +The coin string of the doubled verifier is split by the *per-run* coin count, +a function of the input length, rather than by halving the string. That keeps +the split point polynomial-time computable from the input alone, which is what +the `positions` and `verdict` conditions need. + +## Main definitions + +- `Complexity.PCPVerifier.squareAt` — two independent runs +- `Complexity.PCPWith` — the class with an explicit soundness error + +## Main results + +- `Complexity.PCPWith_square`, `Complexity.mem_PCP_of_PCPWith` — any soundness + error below one can be driven under one half +-/ + +@[expose] public section + +namespace Complexity + +namespace PCPVerifier + +variable (V : PCPVerifier) (t : ℕ → ℕ) + +/-- The coins of the first run. -/ +def fstCoins (t : ℕ → ℕ) (x ρ : List Bool) : List Bool := ρ.take (t x.length) + +/-- The coins of the second run. -/ +def sndCoins (t : ℕ → ℕ) (x ρ : List Bool) : List Bool := ρ.drop (t x.length) + +/-- The queries of two independent runs, one after the other. -/ +def sqPositions (x ρ : List Bool) : List ℕ := + V.positions x (fstCoins t x ρ) ++ V.positions x (sndCoins t x ρ) + +/-- The input, out of a verdict argument `pair (pair x ρ) a`. -/ +def vX (z : List Bool) : List Bool := pairFst (pairFst z) + +/-- The coins, out of a verdict argument. -/ +def vR (z : List Bool) : List Bool := pairSnd (pairFst z) + +/-- The answers, out of a verdict argument. -/ +def vA (z : List Bool) : List Bool := pairSnd z + +/-- The verdict of the doubled verifier: both runs accept. The answers of the +first run are the first `|positions|` of them. -/ +def sqVerdict : Language := + {z | pair (pair (vX z) (fstCoins t (vX z) (vR z))) + ((vA z).take (V.positions (vX z) (fstCoins t (vX z) (vR z))).length) ∈ V.verdict + ∧ pair (pair (vX z) (sndCoins t (vX z) (vR z))) + ((vA z).drop (V.positions (vX z) (fstCoins t (vX z) (vR z))).length) ∈ V.verdict} + +/-! ### The pieces are polynomial time -/ + +section FP + +variable {V t} +variable {f : List Bool → List Bool} +variable (ht : (fun x : List Bool => List.replicate (t x.length) true) ∈ FP) + +theorem vX_mem_FP : vX ∈ FP := mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + +theorem vR_mem_FP : vR ∈ FP := mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + +theorem vA_mem_FP : vA ∈ FP := Cobham.sndBlock_mem_FP + +include ht in +theorem fstCoinsFn_mem_FP {a b : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) : + (fun z => fstCoins t (a z) (b z)) ∈ FP := by + have hlen : (fun z => List.replicate (t (a z).length) true) ∈ FP := by + have := mem_FP_comp ha ht + simpa using this + have := Cobham.takeLenFn_mem_FP hlen hb + refine mem_FP_of_eq this fun z => ?_ + rw [fstCoins, List.length_replicate] + +include ht in +theorem sndCoinsFn_mem_FP {a b : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) : + (fun z => sndCoins t (a z) (b z)) ∈ FP := by + have hlen : (fun z => List.replicate (t (a z).length) true) ∈ FP := by + have := mem_FP_comp ha ht + simpa using this + have := dropLenFn_mem_FP hlen hb + refine mem_FP_of_eq this fun z => ?_ + rw [sndCoins, List.length_replicate] + +include ht in +/-- The doubled verifier's query list is polynomial-time computable. -/ +theorem sqPositions_mem (hf : f ∈ FP) + (hfspec : ∀ x rr : List Bool, + f (pair x rr) = DataEncode.bitstringEncode (V.positions x rr)) : + ∃ g ∈ FP, ∀ x ρ : List Bool, + g (pair x ρ) = DataEncode.bitstringEncode (sqPositions V t x ρ) := by + have hx : (fun z : List Bool => pairFst z) ∈ FP := Cobham.fstBlock_mem_FP + have hr : (fun z : List Bool => pairSnd z) ∈ FP := Cobham.sndBlock_mem_FP + have h1 : (fun z : List Bool => f (pair (pairFst z) + (fstCoins t (pairFst z) (pairSnd z)))) ∈ FP := by + have := mem_FP_comp + (Cobham.pairFn_mem_FP hx (fstCoinsFn_mem_FP ht hx hr)) hf + simpa using this + have h2 : (fun z : List Bool => f (pair (pairFst z) + (sndCoins t (pairFst z) (pairSnd z)))) ∈ FP := by + have := mem_FP_comp + (Cobham.pairFn_mem_FP hx (sndCoinsFn_mem_FP ht hx hr)) hf + simpa using this + refine ⟨fun z => false :: (posInner (f (pair (pairFst z) + (fstCoins t (pairFst z) (pairSnd z)))) + ++ posInner (f (pair (pairFst z) + (sndCoins t (pairFst z) (pairSnd z))))) ++ [true], ?_, ?_⟩ + · have hcat := Cobham.appendFn_mem_FP (posInner_mem_FP h1) (posInner_mem_FP h2) + have hcons := mem_FP_comp hcat (Cobham.cons_mem_FP false) + have := Cobham.appendFn_mem_FP hcons (constFn_mem_FP [true]) + refine mem_FP_of_eq this fun z => ?_ + simp + · intro x ρ + show false :: (posInner (f (pair (pairFst (pair x ρ)) + (fstCoins t (pairFst (pair x ρ)) (pairSnd (pair x ρ))))) + ++ posInner (f (pair (pairFst (pair x ρ)) + (sndCoins t (pairFst (pair x ρ)) (pairSnd (pair x ρ)))))) ++ [true] + = _ + rw [pairFst_pair, pairSnd_pair, hfspec, hfspec, sqPositions, + bitstringEncode_append] + +include ht in +/-- The doubled verifier's verdict is polynomial-time decidable. -/ +theorem sqVerdict_mem_P (hf : f ∈ FP) + (hfspec : ∀ x rr : List Bool, + f (pair x rr) = DataEncode.bitstringEncode (V.positions x rr)) : + sqVerdict V t ∈ P := by + have hc1 : (fun z => fstCoins t (vX z) (vR z)) ∈ FP := + fstCoinsFn_mem_FP ht vX_mem_FP vR_mem_FP + have hc2 : (fun z => sndCoins t (vX z) (vR z)) ∈ FP := + sndCoinsFn_mem_FP ht vX_mem_FP vR_mem_FP + have hfv : (fun z => f (pair (vX z) (fstCoins t (vX z) (vR z)))) ∈ FP := by + have := mem_FP_comp (Cobham.pairFn_mem_FP vX_mem_FP hc1) hf + simpa using this + have hn : (fun z => posCount (f (pair (vX z) (fstCoins t (vX z) (vR z))))) ∈ FP := + posCount_mem_FP hfv + have hnlen : ∀ z, (posCount (f (pair (vX z) (fstCoins t (vX z) (vR z))))).length + = (V.positions (vX z) (fstCoins t (vX z) (vR z))).length := by + intro z + rw [hfspec, posCount_eq, List.length_replicate] + have hA : (fun z => pair (pair (vX z) (fstCoins t (vX z) (vR z))) + ((vA z).take (V.positions (vX z) (fstCoins t (vX z) (vR z))).length)) ∈ FP := by + have := Cobham.pairFn_mem_FP (Cobham.pairFn_mem_FP vX_mem_FP hc1) + (Cobham.takeLenFn_mem_FP hn vA_mem_FP) + refine mem_FP_of_eq this fun z => ?_ + rw [hnlen] + have hB : (fun z => pair (pair (vX z) (sndCoins t (vX z) (vR z))) + ((vA z).drop (V.positions (vX z) (fstCoins t (vX z) (vR z))).length)) ∈ FP := by + have := Cobham.pairFn_mem_FP (Cobham.pairFn_mem_FP vX_mem_FP hc2) + (dropLenFn_mem_FP hn vA_mem_FP) + refine mem_FP_of_eq this fun z => ?_ + rw [hnlen] + exact P_inter (mem_P_preimage hA V.verdict_mem) (mem_P_preimage hB V.verdict_mem) + +end FP + +/-- **Two independent runs**, as a verifier in its own right. -/ +noncomputable def squareAt (V : PCPVerifier) (t : ℕ → ℕ) + (ht : (fun x : List Bool => List.replicate (t x.length) true) ∈ FP) : PCPVerifier where + positions := V.sqPositions t + positions_mem := by + obtain ⟨f, hf, hfspec⟩ := V.positions_mem + exact sqPositions_mem ht hf hfspec + verdict := V.sqVerdict t + verdict_mem := by + obtain ⟨f, hf, hfspec⟩ := V.positions_mem + exact sqVerdict_mem_P ht hf hfspec + +@[simp] theorem positions_squareAt (ht : (fun x : List Bool => + List.replicate (t x.length) true) ∈ FP) (x ρ : List Bool) : + (V.squareAt t ht).positions x ρ = V.sqPositions t x ρ := rfl + +/-! ### Splitting the coin string -/ + +theorem toList_take (a b : ℕ) (ρ : Fin (a + b) → Bool) : + (BitString.toList ρ).take a = BitString.toList (blockFst a b ρ) := by + refine List.ext_getElem (by simp) fun i h1 h2 => ?_ + have hi : i < a := by simpa using h2 + rw [List.getElem_take] + rw [BitString.getElem_toList ρ ⟨i, by omega⟩, + BitString.getElem_toList (blockFst a b ρ) ⟨i, hi⟩] + rfl + +theorem toList_drop (a b : ℕ) (ρ : Fin (a + b) → Bool) : + (BitString.toList ρ).drop a = BitString.toList (blockSnd a b ρ) := by + refine List.ext_getElem (by simp) fun i h1 h2 => ?_ + have hi : i < b := by simpa using h2 + rw [List.getElem_drop] + rw [BitString.getElem_toList ρ ⟨a + i, by omega⟩, + BitString.getElem_toList (blockSnd a b ρ) ⟨i, hi⟩] + rfl + +/-! ### What the doubled verifier accepts -/ + +theorem accepts_squareAt (ht : (fun x : List Bool => + List.replicate (t x.length) true) ∈ FP) (x π ρ : List Bool) : + (V.squareAt t ht).Accepts x π ρ + ↔ V.Accepts x π (fstCoins t x ρ) ∧ V.Accepts x π (sndCoins t x ρ) := by + have hlen : (answers π (V.positions x (fstCoins t x ρ))).length + = (V.positions x (fstCoins t x ρ)).length := by + rw [answers, List.length_map] + rw [Accepts, positions_squareAt, sqPositions, answers, List.map_append] + show _ ∈ V.sqVerdict t ↔ _ + rw [sqVerdict, Set.mem_setOf_eq] + simp only [vX, vR, vA, pairFst_pair, pairSnd_pair] + rw [show List.map (fun i => π.getD i false) (V.positions x (fstCoins t x ρ)) + = answers π (V.positions x (fstCoins t x ρ)) from rfl, + show List.map (fun i => π.getD i false) (V.positions x (sndCoins t x ρ)) + = answers π (V.positions x (sndCoins t x ρ)) from rfl, + ← hlen, List.take_left, List.drop_left] + rfl + +open Classical in +theorem acceptEvent_squareAt (ht : (fun x : List Bool => + List.replicate (t x.length) true) ∈ FP) (x π : List Bool) {T : ℕ} + (hT : t x.length = T) : + (V.squareAt t ht).acceptEvent (T + T) x π + = Finset.univ.filter (fun ρ : Fin (T + T) → Bool => + V.Accepts x π (BitString.toList (blockFst T T ρ)) + ∧ V.Accepts x π (BitString.toList (blockSnd T T ρ))) := by + classical + ext ρ + simp only [acceptEvent, Finset.mem_filter, Finset.mem_univ, true_and] + rw [accepts_squareAt, fstCoins, sndCoins, hT, toList_take, toList_drop] + +open Classical in +theorem eventProb_acceptEvent_squareAt (ht : (fun x : List Bool => + List.replicate (t x.length) true) ∈ FP) (x π : List Bool) {T : ℕ} + (hT : t x.length = T) : + eventProb ((V.squareAt t ht).acceptEvent (T + T) x π) + = eventProb (V.acceptEvent T x π) * eventProb (V.acceptEvent T x π) := by + classical + rw [acceptEvent_squareAt V t ht x π hT] + rw [eventProb_block (P := fun σ : Fin T → Bool => V.Accepts x π (BitString.toList σ)) + (Q := fun σ : Fin T → Bool => V.Accepts x π (BitString.toList σ))] + rfl + +end PCPVerifier + +/-! ### Amplifying the class -/ + +/-- The `PCP` class with an explicit soundness error. -/ +def PCPWith (r q : ℕ → ℕ) (s : ℚ) : Set Language := + {L | ∃ V : PCPVerifier, V.QueryBounded q ∧ + (∀ x ∈ L, ∃ π : List Bool, eventProb (V.acceptEvent (r x.length) x π) = 1) ∧ + (∀ x ∉ L, ∀ π : List Bool, eventProb (V.acceptEvent (r x.length) x π) ≤ s)} + +theorem PCPWith_half (r q : ℕ → ℕ) : PCPWith r q (1 / 2) = PCP r q := rfl + +/-- **Two runs square the error**, at twice the randomness and twice the +queries. -/ +theorem PCPWith_square {r q : ℕ → ℕ} {s : ℚ} (hs : 0 ≤ s) + (hr : (fun x : List Bool => List.replicate (r x.length) true) ∈ FP) + {L : Language} (hL : L ∈ PCPWith r q s) : + L ∈ PCPWith (fun n => r n + r n) (fun n => q n + q n) (s * s) := by + obtain ⟨V, hQ, hcomp, hsound⟩ := hL + refine ⟨V.squareAt r hr, ?_, ?_, ?_⟩ + · intro x ρ + rw [PCPVerifier.positions_squareAt, PCPVerifier.sqPositions, List.length_append] + exact Nat.add_le_add (hQ _ _) (hQ _ _) + · intro x hx + obtain ⟨π, hπ⟩ := hcomp x hx + refine ⟨π, ?_⟩ + rw [PCPVerifier.eventProb_acceptEvent_squareAt V r hr x π rfl, hπ] + norm_num + · intro x hx π + rw [PCPVerifier.eventProb_acceptEvent_squareAt V r hr x π rfl] + exact mul_le_mul (hsound x hx π) (hsound x hx π) (eventProb_nonneg _) hs + +/-! ### Driving the error below one half -/ + +theorem PCPWith_congr {r r' q q' : ℕ → ℕ} {s : ℚ} (hr : ∀ n, r n = r' n) + (hq : ∀ n, q n = q' n) : PCPWith r q s = PCPWith r' q' s := by + have h1 : r = r' := funext hr + have h2 : q = q' := funext hq + rw [h1, h2] + +theorem PCPWith_mono {r q : ℕ → ℕ} {s s' : ℚ} (h : s ≤ s') : + PCPWith r q s ⊆ PCPWith r q s' := by + rintro L ⟨V, hQ, hc, hsound⟩ + exact ⟨V, hQ, hc, fun x hx π => le_trans (hsound x hx π) h⟩ + +theorem constructible_double {r : ℕ → ℕ} + (hr : (fun x : List Bool => List.replicate (r x.length) true) ∈ FP) : + (fun x : List Bool => List.replicate (r x.length + r x.length) true) ∈ FP := by + have := Cobham.appendFn_mem_FP hr hr + refine mem_FP_of_eq this fun x => ?_ + rw [← List.replicate_add] + +/-- **Repeated doubling.** After `j` doublings the error is `s ^ (2 ^ j)`. -/ +theorem PCPWith_iterate (j : ℕ) : ∀ {r q : ℕ → ℕ} {s : ℚ}, 0 ≤ s → + (fun x : List Bool => List.replicate (r x.length) true) ∈ FP → + ∀ {L : Language}, L ∈ PCPWith r q s → + L ∈ PCPWith (fun n => 2 ^ j * r n) (fun n => 2 ^ j * q n) (s ^ 2 ^ j) := by + induction j with + | zero => + intro r q s _ _ L hL + rw [PCPWith_congr (r' := r) (q' := q) (fun n => by ring) (fun n => by ring)] + simpa using hL + | succ j ih => + intro r q s hs hr L hL + have hsq := PCPWith_square hs hr hL + have hstep := ih (s := s * s) (by positivity) (constructible_double hr) hsq + have hr' : ∀ n, 2 ^ j * (r n + r n) = 2 ^ (j + 1) * r n := by + intro n; ring + have hq' : ∀ n, 2 ^ j * (q n + q n) = 2 ^ (j + 1) * q n := by + intro n; ring + rw [PCPWith_congr hr' hq'] at hstep + have hpow : (s * s) ^ 2 ^ j = s ^ 2 ^ (j + 1) := by + rw [← sq, ← pow_mul, pow_succ] + ring_nf + rwa [hpow] at hstep + +/-- **Any error below one can be driven under one half.** -/ +theorem mem_PCP_of_PCPWith {r q : ℕ → ℕ} {s : ℚ} (hs0 : 0 ≤ s) (hs1 : s < 1) + (hr : (fun x : List Bool => List.replicate (r x.length) true) ∈ FP) + {L : Language} (hL : L ∈ PCPWith r q s) : + ∃ j : ℕ, L ∈ PCP (fun n => 2 ^ j * r n) (fun n => 2 ^ j * q n) := by + obtain ⟨m, hm⟩ := exists_pow_lt_of_lt_one (by norm_num : (0 : ℚ) < 1 / 2) hs1 + refine ⟨m, ?_⟩ + have hle : s ^ 2 ^ m ≤ s ^ m := by + refine pow_le_pow_of_le_one hs0 (le_of_lt hs1) ?_ + exact Nat.le_of_lt (Nat.lt_two_pow_self) + rw [← PCPWith_half] + exact PCPWith_mono (le_trans hle (le_of_lt hm)) (PCPWith_iterate m hs0 hr hL) + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/StripTrailing.lean b/Complexitylib/Classes/PCP/Internal/StripTrailing.lean new file mode 100644 index 00000000..94e786b8 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/StripTrailing.lean @@ -0,0 +1,171 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.BinValLE +public import Complexitylib.Classes.P.Cobham.Internal.HeadOps +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.PCP.Internal.BinToUnary +public import Complexitylib.Encoding.Pairing + +/-! +# The canonical bits of a number + +`DataEncode` writes a natural number as `Nat.bits`, its little-endian bits with +no trailing zero. A fixed-width counter, which is what a polynomial-time machine +can produce, carries trailing zeros; dropping them is the missing step between +the two. + +Dropping trailing zeros is a right fold — what to do with a bit depends on +whether everything after it vanished — so `recFoldClamp` expresses it directly, +with no reversal. + +## Main definitions + +- `Complexity.stripTrailing` — drop trailing zeros + +## Main results + +- `Complexity.stripTrailing_eq_bits` — the result is `Nat.bits` of the value +- `Complexity.stripTrailing_mem_FP` — it is polynomial time +-/ + +@[expose] public section + +namespace Complexity + +/-- Drop trailing zeros from a little-endian bit string. -/ +def stripTrailing : List Bool → List Bool + | [] => [] + | false :: t => if stripTrailing t = [] then [] else false :: stripTrailing t + | true :: t => true :: stripTrailing t + +@[simp] theorem stripTrailing_nil : stripTrailing [] = [] := rfl + +theorem stripTrailing_false (t : List Bool) : + stripTrailing (false :: t) = + if stripTrailing t = [] then [] else false :: stripTrailing t := rfl + +theorem stripTrailing_true (t : List Bool) : + stripTrailing (true :: t) = true :: stripTrailing t := rfl + +theorem length_stripTrailing (l : List Bool) : (stripTrailing l).length ≤ l.length := by + induction l with + | nil => simp + | cons b t ih => + cases b + · rw [stripTrailing_false] + split + · simp + · simp only [List.length_cons] + omega + · rw [stripTrailing_true] + simp only [List.length_cons] + omega + +/-- **Dropping trailing zeros gives the canonical bits.** -/ +theorem stripTrailing_eq_bits (l : List Bool) : stripTrailing l = (binValLE l).bits := by + induction l with + | nil => rfl + | cons b t ih => + cases b + · rw [stripTrailing_false, ih, binValLE_cons_false] + by_cases h : (binValLE t).bits = [] + · rw [if_pos h] + have h0 : binValLE t = 0 := by + have hb := binValLE_bits (binValLE t) + rw [h] at hb + simpa using hb.symm + rw [h0] + simp + · rw [if_neg h] + have hne : binValLE t ≠ 0 := by + intro h0 + rw [h0, Nat.zero_bits] at h + exact h rfl + rw [Nat.bit0_bits _ hne] + · rw [stripTrailing_true, ih, binValLE_cons_true, Nat.bit1_bits] + +/-! ### Polynomial time -/ + +/-- The fold step on a zero. -/ +def stripZero (z : List Bool) : List Bool := + Cobham.selectHead (emptyFlag (pairSnd (pairFst z))) [] + (false :: pairSnd (pairFst z)) + +/-- The fold step on a one. -/ +def stripOne (z : List Bool) : List Bool := + true :: pairSnd (pairFst z) + +theorem stripZero_mem_FP : stripZero ∈ FP := by + have h : (fun z : List Bool => pairSnd (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + exact Cobham.selectHeadFn_mem_FP (emptyFlagFn_mem_FP h) (constFn_mem_FP []) + (mem_FP_comp h (Cobham.cons_mem_FP false)) + +theorem stripOne_mem_FP : stripOne ∈ FP := by + have h : (fun z : List Bool => pairSnd (pairFst z)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + exact mem_FP_comp h (Cobham.cons_mem_FP true) + +/-- The fold computes `stripTrailing`, as long as the clamp allows the answer. -/ +theorem recFoldClamp_stripTrailing (bound : ℕ) (W : List Bool) : + ∀ l : List Bool, l.length ≤ bound → + Cobham.recFoldClamp stripZero stripOne bound [] W l = stripTrailing l := by + intro l + induction l with + | nil => + intro _ + rw [Cobham.recFoldClamp] + simp + | cons b t ih => + intro hb + have hb' : t.length ≤ bound := by + simp only [List.length_cons] at hb + omega + rw [Cobham.recFoldClamp, ih hb'] + have hstate : pairSnd (pairFst + (pair (pair W (stripTrailing t)) t)) = stripTrailing t := by + rw [pairFst_pair, pairSnd_pair] + have hlt : (stripTrailing t).length ≤ t.length := length_stripTrailing t + cases b + · show (stripZero _).take bound = _ + rw [stripZero, hstate, stripTrailing_false] + cases hs : stripTrailing t with + | nil => + rw [emptyFlag_nil, selectHead_cons_true, if_pos rfl] + simp + | cons c s => + rw [emptyFlag_cons, selectHead_cons_false, if_neg (by simp)] + refine List.take_of_length_le ?_ + simp only [List.length_cons] + rw [hs] at hlt + simp only [List.length_cons] at hlt + simp only [List.length_cons] at hb + omega + · show (stripOne _).take bound = _ + rw [stripOne, hstate, stripTrailing_true] + refine List.take_of_length_le ?_ + simp only [List.length_cons] at hb ⊢ + omega + +/-- Dropping trailing zeros, on `pair anything bits`. -/ +def stripFn (z : List Bool) : List Bool := + Cobham.recFoldClamp stripZero stripOne z.length [] (pairFst z) + (pairSnd z) + +theorem stripFn_mem_FP : stripFn ∈ FP := by + have := Cobham.recFoldClamp_mem_FP stripZero_mem_FP stripOne_mem_FP + (constFn_mem_FP []) (Polynomial.X) + refine mem_FP_of_eq this fun z => ?_ + rw [stripFn] + simp + +theorem stripFn_eq (z : List Bool) : + stripFn z = stripTrailing (pairSnd z) := by + refine recFoldClamp_stripTrailing _ _ _ ?_ + exact pairSnd_length_le z + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/SubsetNP.lean b/Complexitylib/Classes/PCP/Internal/SubsetNP.lean new file mode 100644 index 00000000..e9e56c44 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/SubsetNP.lean @@ -0,0 +1,327 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Defs + +/-! +# From proofs to answer tables + +The combinatorial heart of `PCP[r, q] ⊆ NP`: a nondeterministic machine cannot +guess the *proof*, which may be astronomically long — the verifier's query +positions are outputs of a polynomial-time function, so they are bounded only by +`2 ^ poly`. What it can guess is the much smaller table of answers at the +positions actually queried. + +This module shows the two are interchangeable. A proof determines a table by +reading it, and conversely any table that satisfies the verifier on every coin +string can be realised by an actual proof — take the list long enough to cover +every position the verifier could ever ask about, which is a finite maximum since +there are finitely many coin strings. + +## Main definitions + +- `PCPVerifier.AcceptsWith` — acceptance when answers come from a table +- `PCPVerifier.maxQuery` — a bound past every position the verifier can query + +## Main results + +- `PCPVerifier.exists_proof_of_table` — a table is a proof +- `PCPVerifier.Consistent`, `PCPVerifier.exists_proof_of_consistent_table` — a + consistent accepted table, indexed by coin string, is a proof +- `PCPVerifier.coinIndex`, `PCPVerifier.tableOf`, `PCPVerifier.Witness` — the + table as a bitstring, in a fixed-stride layout +- `PCPVerifier.witnessOf`, `PCPVerifier.tableOf_witnessOf` — the witness a proof + induces +- `PCPVerifier.eventProb_acceptEvent_eq_one_iff` — certain acceptance, unfolded +-/ + +@[expose] public section + +namespace Complexity + +namespace PCPVerifier + +variable (V : PCPVerifier) + +/-- The verifier accepts when the answers are read off the table `f`. -/ +def AcceptsWith (V : PCPVerifier) (x : List Bool) (f : ℕ → Bool) (ρ : List Bool) : Prop := + pair (pair x ρ) ((V.positions x ρ).map f) ∈ V.verdict + +/-! ### Realising a table by a proof -/ + +/-- A bound past every position the verifier can query on `x` with `t` coins. -/ +noncomputable def maxQuery (V : PCPVerifier) (t : ℕ) (x : List Bool) : ℕ := + (Finset.univ.sup fun ρ : Fin t → Bool => + (V.positions x (BitString.toList ρ)).foldr max 0) + 1 + +theorem lt_maxQuery {t : ℕ} {x : List Bool} {ρ : Fin t → Bool} {p : ℕ} + (hp : p ∈ V.positions x (BitString.toList ρ)) : p < V.maxQuery t x := by + have h1 : p ≤ (V.positions x (BitString.toList ρ)).foldr max 0 := List.le_max_of_le' 0 hp le_rfl + have h2 : (V.positions x (BitString.toList ρ)).foldr max 0 + ≤ Finset.univ.sup fun σ : Fin t → Bool => + (V.positions x (BitString.toList σ)).foldr max 0 := + Finset.le_sup (f := fun σ : Fin t → Bool => + (V.positions x (BitString.toList σ)).foldr max 0) (Finset.mem_univ ρ) + rw [maxQuery] + omega + +/-- **A table is a proof.** A table accepted on every coin string is realised by +an honest proof: the list of its values up to the largest position the verifier +could query. -/ +theorem exists_proof_of_table (t : ℕ) (x : List Bool) (f : ℕ → Bool) + (h : ∀ ρ : Fin t → Bool, V.AcceptsWith x f (BitString.toList ρ)) : + ∃ π : List Bool, ∀ ρ : Fin t → Bool, V.Accepts x π (BitString.toList ρ) := by + classical + refine ⟨(List.range (V.maxQuery t x)).map f, fun ρ => ?_⟩ + have hread : ∀ p ∈ V.positions x (BitString.toList ρ), + ((List.range (V.maxQuery t x)).map f).getD p false = f p := by + intro p hp + have hlt : p < V.maxQuery t x := V.lt_maxQuery hp + have hlen : ((List.range (V.maxQuery t x)).map f).length = V.maxQuery t x := by + simp + rw [← List.getElem_eq_getD (h := by rw [hlen]; exact hlt)] + simp + have hmap : (V.positions x (BitString.toList ρ)).map + (fun i => ((List.range (V.maxQuery t x)).map f).getD i false) + = (V.positions x (BitString.toList ρ)).map f := + List.map_congr_left hread + rw [Accepts, answers, hmap] + exact h ρ + +/-! ### Tables indexed by coin string -/ + +/-- A table assigns answers to every coin string. It is *consistent* when two +coin strings that query the same position receive the same answer — the +condition a dishonest prover would violate, and the one that lets a table be +read back as a single proof. -/ +def Consistent (V : PCPVerifier) (t : ℕ) (x : List Bool) + (tbl : (Fin t → Bool) → List Bool) : Prop := + ∀ (ρ ρ' : Fin t → Bool) (i i' : ℕ) (p : ℕ), + (V.positions x (BitString.toList ρ))[i]? = some p → + (V.positions x (BitString.toList ρ'))[i']? = some p → + (tbl ρ)[i]? = (tbl ρ')[i']? + +open Classical in +/-- The position-indexed reading of a table. -/ +noncomputable def tableFun (V : PCPVerifier) (t : ℕ) (x : List Bool) + (tbl : (Fin t → Bool) → List Bool) (p : ℕ) : Bool := + if ∃ (ρ : Fin t → Bool) (i : ℕ), + (V.positions x (BitString.toList ρ))[i]? = some p ∧ (tbl ρ)[i]? = some true + then true else false + +/-- On a consistent table the reading returns the recorded answer. -/ +theorem tableFun_eq {t : ℕ} {x : List Bool} {tbl : (Fin t → Bool) → List Bool} + (hcons : V.Consistent t x tbl) {ρ : Fin t → Bool} {i p : ℕ} {b : Bool} + (hpos : (V.positions x (BitString.toList ρ))[i]? = some p) + (hans : (tbl ρ)[i]? = some b) : V.tableFun t x tbl p = b := by + classical + cases b with + | true => + rw [tableFun, if_pos ⟨ρ, i, hpos, hans⟩] + | false => + rw [tableFun, if_neg] + rintro ⟨ρ', i', hpos', hans'⟩ + have := hcons ρ ρ' i i' p hpos hpos' + rw [hans, hans'] at this + exact absurd this (by simp) + +/-- **A consistent accepted table is a proof.** -/ +theorem exists_proof_of_consistent_table (t : ℕ) (x : List Bool) + (tbl : (Fin t → Bool) → List Bool) + (hlen : ∀ ρ : Fin t → Bool, + (tbl ρ).length = (V.positions x (BitString.toList ρ)).length) + (hcons : V.Consistent t x tbl) + (hacc : ∀ ρ : Fin t → Bool, + pair (pair x (BitString.toList ρ)) (tbl ρ) ∈ V.verdict) : + ∃ π : List Bool, ∀ ρ : Fin t → Bool, V.Accepts x π (BitString.toList ρ) := by + classical + refine V.exists_proof_of_table t x (V.tableFun t x tbl) fun ρ => ?_ + have hmap : (V.positions x (BitString.toList ρ)).map (V.tableFun t x tbl) = tbl ρ := by + refine List.ext_getElem? fun i => ?_ + rcases hi : (V.positions x (BitString.toList ρ))[i]? with _ | p + · have hlen1 : (V.positions x (BitString.toList ρ)).length ≤ i := + List.getElem?_eq_none_iff.mp hi + rw [List.getElem?_map, hi] + exact (List.getElem?_eq_none_iff.mpr (by rw [hlen]; exact hlen1)).symm + · have hilt : i < (V.positions x (BitString.toList ρ)).length := + List.getElem?_eq_some_iff.mp hi |>.1 + have hilt' : i < (tbl ρ).length := by rw [hlen]; exact hilt + obtain ⟨b, hb⟩ : ∃ b, (tbl ρ)[i]? = some b := + ⟨(tbl ρ)[i]'hilt', List.getElem?_eq_getElem hilt'⟩ + rw [List.getElem?_map, hi, hb] + exact congrArg some (V.tableFun_eq hcons hi hb) + rw [AcceptsWith, hmap] + exact hacc ρ + +/-! ### The characterisation -/ + +/-- The table a proof induces is consistent: both entries read the same +position of the same proof. -/ +theorem consistent_of_proof (t : ℕ) (x π : List Bool) : + V.Consistent t x fun ρ => answers π (V.positions x (BitString.toList ρ)) := by + intro ρ ρ' i i' p hpos hpos' + show (List.map (fun i => π.getD i false) (V.positions x (BitString.toList ρ)))[i]? + = (List.map (fun i => π.getD i false) (V.positions x (BitString.toList ρ')))[i']? + rw [List.getElem?_map, List.getElem?_map, hpos, hpos'] + +/-! ### Encoding a table as a witness -/ + +/-- The digits of a coin string, as an element of `Fin 2` per coin. -/ +def coinDigits {t : ℕ} (ρ : Fin t → Bool) : Fin t → Fin 2 := + fun i => if ρ i then 1 else 0 + +theorem coinDigits_injective {t : ℕ} : Function.Injective (coinDigits (t := t)) := by + intro ρ ρ' h + funext i + have hi := congrFun h i + rw [coinDigits, coinDigits] at hi + revert hi + cases ρ i <;> cases ρ' i <;> decide + +/-- A canonical index for each coin string: the value of its digits read as a +binary numeral. This is deliberately an explicit equivalence rather than one +obtained from `Fintype.equivFinOfCardEq`, so that the layout of a witness is +computable — the reduction of a verifier to a CNF formula depends on it. -/ +def coinIndex {t : ℕ} (ρ : Fin t → Bool) : ℕ := + (finFunctionFinEquiv (coinDigits ρ)).val + +theorem coinIndex_lt {t : ℕ} (ρ : Fin t → Bool) : coinIndex ρ < 2 ^ t := + (finFunctionFinEquiv (coinDigits ρ)).isLt + +theorem coinIndex_injective {t : ℕ} : Function.Injective (coinIndex (t := t)) := + fun _ _ h => coinDigits_injective (finFunctionFinEquiv.injective (Fin.ext h)) + +/-- The coin string with a given index — the inverse of `coinIndex`. -/ +def coinOfIndex {t : ℕ} (c : Fin (2 ^ t)) : Fin t → Bool := + fun i => finFunctionFinEquiv.symm c i == 1 + +theorem coinOfIndex_coinIndex {t : ℕ} (ρ : Fin t → Bool) (h : coinIndex ρ < 2 ^ t) : + coinOfIndex ⟨coinIndex ρ, h⟩ = ρ := by + have hfin : (⟨coinIndex ρ, h⟩ : Fin (2 ^ t)) = finFunctionFinEquiv (coinDigits ρ) := + Fin.ext rfl + funext i + rw [coinOfIndex, hfin, Equiv.symm_apply_apply, coinDigits] + cases ρ i <;> decide + +/-- The table a witness encodes: the answers for coin string `ρ` sit in the +slots `coinIndex ρ * Q, …` of the witness, a fixed stride apart. -/ +noncomputable def tableOf (V : PCPVerifier) (t Q : ℕ) (x : List Bool) (w : List Bool) : + (Fin t → Bool) → List Bool := + fun ρ => (List.range (V.positions x (BitString.toList ρ)).length).map + fun i => w.getD (coinIndex ρ * Q + i) false + +theorem length_tableOf (t Q : ℕ) (x w : List Bool) (ρ : Fin t → Bool) : + (V.tableOf t Q x w ρ).length = (V.positions x (BitString.toList ρ)).length := by + rw [tableOf, List.length_map, List.length_range] + +/-- The witness relation: the encoded table is consistent and accepted on every +coin string. -/ +def Witness (V : PCPVerifier) (t Q : ℕ) (x w : List Bool) : Prop := + V.Consistent t x (V.tableOf t Q x w) ∧ + ∀ ρ : Fin t → Bool, + pair (pair x (BitString.toList ρ)) (V.tableOf t Q x w ρ) ∈ V.verdict + +/-- A witness yields a proof. -/ +theorem exists_proof_of_witness {t Q : ℕ} {x w : List Bool} (h : V.Witness t Q x w) : + ∃ π : List Bool, ∀ ρ : Fin t → Bool, V.Accepts x π (BitString.toList ρ) := + V.exists_proof_of_consistent_table t x (V.tableOf t Q x w) (V.length_tableOf t Q x w) + h.1 h.2 + +open Classical in +/-- The witness a proof induces: each coin string's answers written into its own +stride of the witness. -/ +noncomputable def witnessOf (V : PCPVerifier) (t Q : ℕ) (x π : List Bool) : List Bool := + (List.range (2 ^ t * Q)).map fun k => + if h : k / Q < 2 ^ t then + π.getD ((V.positions x (BitString.toList (coinOfIndex ⟨k / Q, h⟩))).getD (k % Q) 0) + false + else false + +theorem length_witnessOf (t Q : ℕ) (x π : List Bool) : + (V.witnessOf t Q x π).length = 2 ^ t * Q := by + rw [witnessOf, List.length_map, List.length_range] + +/-- The witness a proof induces encodes exactly the proof's own answers. -/ +theorem tableOf_witnessOf {t Q : ℕ} (x π : List Bool) (hQ0 : 0 < Q) + (hQ : ∀ ρ : Fin t → Bool, (V.positions x (BitString.toList ρ)).length ≤ Q) + (ρ : Fin t → Bool) : + V.tableOf t Q x (V.witnessOf t Q x π) ρ + = answers π (V.positions x (BitString.toList ρ)) := by + classical + refine List.ext_getElem ?_ fun i h1 h2 => ?_ + · rw [V.length_tableOf t Q x (V.witnessOf t Q x π) ρ] + show _ = (List.map (fun i => π.getD i false) _).length + rw [List.length_map] + · have hilt : i < (V.positions x (BitString.toList ρ)).length := by + rw [V.length_tableOf t Q x (V.witnessOf t Q x π) ρ] at h1 + exact h1 + have hiQ : i < Q := lt_of_lt_of_le hilt (hQ ρ) + have hkey : coinIndex ρ * Q + i < 2 ^ t * Q := by + have hc := coinIndex_lt ρ + calc coinIndex ρ * Q + i < coinIndex ρ * Q + Q := by omega + _ = (coinIndex ρ + 1) * Q := by ring + _ ≤ 2 ^ t * Q := Nat.mul_le_mul_right _ (by omega) + have hdiv : (coinIndex ρ * Q + i) / Q = coinIndex ρ := by + rw [mul_comm, Nat.mul_add_div hQ0, Nat.div_eq_of_lt hiQ, Nat.add_zero] + have hmod : (coinIndex ρ * Q + i) % Q = i := by + rw [mul_comm, Nat.mul_add_mod, Nat.mod_eq_of_lt hiQ] + have hrho : ∀ h : (coinIndex ρ * Q + i) / Q < 2 ^ t, + coinOfIndex ⟨(coinIndex ρ * Q + i) / Q, h⟩ = ρ := by + intro h + have hfin : (⟨(coinIndex ρ * Q + i) / Q, h⟩ : Fin (2 ^ t)) + = ⟨coinIndex ρ, coinIndex_lt ρ⟩ := Fin.ext hdiv + rw [hfin, coinOfIndex_coinIndex] + have hdivlt : (coinIndex ρ * Q + i) / Q < 2 ^ t := by + rw [hdiv] + exact coinIndex_lt ρ + have hwit : (V.witnessOf t Q x π).getD (coinIndex ρ * Q + i) false + = π.getD ((V.positions x (BitString.toList ρ)).getD i 0) false := by + have hlen : coinIndex ρ * Q + i < (V.witnessOf t Q x π).length := by + rw [V.length_witnessOf t Q x π] + exact hkey + rw [← List.getElem_eq_getD (h := hlen)] + show ((List.range (2 ^ t * Q)).map _)[coinIndex ρ * Q + i] = _ + rw [List.getElem_map, List.getElem_range, dif_pos hdivlt, hrho hdivlt, hmod] + show ((List.range (V.positions x (BitString.toList ρ)).length).map + fun i => (V.witnessOf t Q x π).getD (coinIndex ρ * Q + i) false)[i] = _ + rw [List.getElem_map, List.getElem_range, hwit] + show _ = (List.map (fun i => π.getD i false) _)[i] + rw [List.getElem_map] + congr 1 + rw [← List.getElem_eq_getD (h := hilt)] + +/-- Certain acceptance is acceptance on every coin string. -/ +theorem eventProb_acceptEvent_eq_one_iff (t : ℕ) (x π : List Bool) : + eventProb (V.acceptEvent t x π) = 1 + ↔ ∀ ρ : Fin t → Bool, V.Accepts x π (BitString.toList ρ) := by + classical + have hcard : (Finset.univ : Finset (Fin t → Bool)).card = 2 ^ t := by + rw [Finset.card_univ, card_finArrowBool] + constructor + · intro h ρ + have huniv : V.acceptEvent t x π = Finset.univ := by + by_contra hne + have hlt : (V.acceptEvent t x π).card < 2 ^ t := by + rw [← hcard] + exact Finset.card_lt_card (Finset.ssubset_univ_iff.mpr hne) + have hpos : (0 : ℚ) < 2 ^ t := by positivity + rw [eventProb, div_eq_one_iff_eq (ne_of_gt hpos)] at h + have : ((V.acceptEvent t x π).card : ℚ) < ((2 : ℚ) ^ t) := by exact_mod_cast hlt + rw [h] at this + exact absurd this (lt_irrefl _) + have hmem : ρ ∈ V.acceptEvent t x π := by rw [huniv]; exact Finset.mem_univ ρ + rw [acceptEvent, Finset.mem_filter] at hmem + exact hmem.2 + · intro h + have huniv : V.acceptEvent t x π = Finset.univ := by + ext ρ + simp only [acceptEvent, Finset.mem_filter, Finset.mem_univ, true_and, iff_true] + exact h ρ + rw [huniv, eventProb_univ] + +end PCPVerifier + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/SubsetNPFinal.lean b/Complexitylib/Classes/PCP/Internal/SubsetNPFinal.lean new file mode 100644 index 00000000..e5afb654 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/SubsetNPFinal.lean @@ -0,0 +1,157 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.P.DecisionFn +public import Complexitylib.Classes.PCP.Internal.ConsistencyLang +public import Complexitylib.Classes.PCP.Internal.GuessVerifyGeneric + +/-! +# `PCP[O(log n), O(1)] ⊆ NP` + +A verifier with logarithmic randomness has polynomially many coin strings, so +its whole behaviour on an input is captured by a table of answers of polynomial +size. This module assembles the checks on such a table — that it has the right +width, that it is consistent, and that the verifier accepts on every coin +string — into a single polynomial-time verifier language, and hands it to the +guess-and-verify bridge. + +The completeness and soundness conditions of `PCP` do the rest: a member has a +proof accepted always, and a non-member has none, since a proof accepted always +would give acceptance probability one rather than at most one half. + +## Main results + +- `Complexity.PCP_subset_NP` — the easy inclusion of the PCP theorem +-/ + +@[expose] public section + +namespace Complexity + +section + +variable (r : ℕ → ℕ) (Q : ℕ) + +/-- The witness has exactly one block per coin string. -/ +noncomputable def lenLang : Language := + {z | (pairSnd z).length = 2 ^ r (pairFst z).length * Q} + +open scoped Complexity in +theorem lenLang_mem_P + (hr : (fun x : List Bool => List.replicate (r x.length) true) ∈ FP) + (hrlog : r =O fun n => Nat.log 2 n) : lenLang r Q ∈ P := by + have ha : (fun z : List Bool => List.replicate (pairSnd z).length false) ∈ FP := + zeroBlockFn_mem_FP Cobham.sndBlock_mem_FP + have hexp : (fun z : List Bool => + List.replicate (2 ^ r (pairFst z).length) true) ∈ FP := by + have := mem_FP_comp Cobham.fstBlock_mem_FP (unaryExp_mem_FP_of_bigO_log hr hrlog) + simpa using this + have hQ : (fun _ : List Bool => List.replicate Q false) ∈ FP := + Cobham.const_replicate_mem_FP Q + have hb : (fun z : List Bool => + List.replicate (2 ^ r (pairFst z).length * Q) false) ∈ FP := by + have := Cobham.mulLenFn_mem_FP hexp hQ + refine mem_FP_of_eq this fun z => ?_ + rw [List.length_replicate, List.length_replicate] + refine mem_P_of_decisionFn (eqFlagFn_mem_FP ha hb) fun z => ?_ + rw [exists_eqFlag_iff] + constructor + · intro h + rw [lenLang, Set.mem_setOf_eq] at h + rw [h] + · intro h + have := congrArg List.length h + rw [List.length_replicate, List.length_replicate] at this + exact this + +variable (V : PCPVerifier) (f : List Bool → List Bool) + +/-- **The verifier language**: the witness has the right shape, is consistent, +and is accepted on every coin string. -/ +noncomputable def witLang : Language := + lenLang r Q ∩ (consLang f r Q ∩ accLang V f r Q) + +open scoped Complexity in +theorem witLang_mem_P (hf : f ∈ FP) + (hr : (fun x : List Bool => List.replicate (r x.length) true) ∈ FP) + (hrlog : r =O fun n => Nat.log 2 n) : witLang r Q V f ∈ P := + P_inter (lenLang_mem_P r Q hr hrlog) + (P_inter (consLang_mem_P f r Q hf hr hrlog) (accLang_mem_P V f r Q hf hr hrlog)) + +theorem mem_witLang_iff + (hfspec : ∀ x rr : List Bool, + f (pair x rr) = DataEncode.bitstringEncode (V.positions x rr)) + (hQ : ∀ x rr : List Bool, (V.positions x rr).length ≤ Q) (x w : List Bool) : + pair x w ∈ witLang r Q V f + ↔ w.length = 2 ^ r x.length * Q ∧ V.Witness (r x.length) Q x w := by + constructor + · rintro ⟨hlen, hcons, hacc⟩ + have hlen' : w.length = 2 ^ r x.length * Q := by + rw [lenLang, Set.mem_setOf_eq, pairFst_pair, pairSnd_pair] at hlen + exact hlen + refine ⟨hlen', ?_, ?_⟩ + · exact (mem_consLang_iff V f r Q hfspec hlen' (hQ x)).mp hcons + · exact (mem_accLang_iff V f r Q hfspec hlen' (hQ x)).mp hacc + · rintro ⟨hlen, hcons, hacc⟩ + refine ⟨?_, ?_, ?_⟩ + · rw [lenLang, Set.mem_setOf_eq, pairFst_pair, pairSnd_pair] + exact hlen + · exact (mem_consLang_iff V f r Q hfspec hlen (hQ x)).mpr hcons + · exact (mem_accLang_iff V f r Q hfspec hlen (hQ x)).mpr hacc + +end + +open scoped Complexity in +/-- **`PCP[O(log n), O(1)] ⊆ NP`.** A verifier's whole behaviour is a table of +answers of polynomial size; guessing that table and checking it is an `NP` +computation. -/ +theorem PCP_subset_NP {r q : ℕ → ℕ} + (hr : (fun x : List Bool => List.replicate (r x.length) true) ∈ FP) + (hrlog : r =O fun n => Nat.log 2 n) (hq : q =O fun _ => 1) : + PCP r q ⊆ NP := by + rintro L ⟨V, hVq, hcomp, hsound⟩ + obtain ⟨K, hK⟩ := exists_const_query_bound hVq hq + obtain ⟨f, hf, hfspec⟩ := V.positions_mem + set Q := K + 1 with hQdef + have hQ : ∀ x rr : List Bool, (V.positions x rr).length ≤ Q := fun x rr => + le_trans (hK x rr) (by omega) + have hQ0 : 0 < Q := by omega + obtain ⟨p₀, hp₀⟩ := exists_poly_two_pow_of_bigO_log hrlog + refine mem_NP_of_poly_witness (p₀ * Polynomial.C Q) + (witLang_mem_P r Q V f hf hr hrlog) ?_ ?_ + · intro x y hy + rw [mem_witLang_iff r Q V f hfspec hQ] at hy + rw [hy.1] + simp only [Polynomial.eval_mul, Polynomial.eval_C] + exact Nat.mul_le_mul_right _ (hp₀ x.length) + · intro x + constructor + · intro hx + obtain ⟨π, hπ⟩ := hcomp x hx + rw [V.eventProb_acceptEvent_eq_one_iff] at hπ + refine ⟨V.witnessOf (r x.length) Q x π, ?_⟩ + rw [mem_witLang_iff r Q V f hfspec hQ] + refine ⟨V.length_witnessOf _ _ _ _, ?_, ?_⟩ + · intro ρ ρ' i i' pp hpos hpos' + rw [V.tableOf_witnessOf x π hQ0 (fun ρ => hQ x _), + V.tableOf_witnessOf x π hQ0 (fun ρ => hQ x _)] + exact V.consistent_of_proof _ x π ρ ρ' i i' pp hpos hpos' + · intro ρ + rw [V.tableOf_witnessOf x π hQ0 (fun ρ => hQ x _)] + exact hπ ρ + · rintro ⟨w, hw⟩ + rw [mem_witLang_iff r Q V f hfspec hQ] at hw + obtain ⟨π, hπ⟩ := V.exists_proof_of_witness hw.2 + by_contra hx + have h1 : eventProb (V.acceptEvent (r x.length) x π) = 1 := + (V.eventProb_acceptEvent_eq_one_iff _ x π).mpr hπ + have h2 := hsound x hx π + rw [h1] at h2 + norm_num at h2 + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/SymbolCodec.lean b/Complexitylib/Classes/PCP/Internal/SymbolCodec.lean new file mode 100644 index 00000000..f9be590e --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/SymbolCodec.lean @@ -0,0 +1,62 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.BinValLE +public import Complexitylib.Classes.PCP.Internal.CoinEnum + +/-! +# Writing symbols as fixed-width blocks + +A proof for a constraint graph is an assignment written out, one fixed-width +block per vertex. This module supplies the block: any finite alphabet small +enough is written as its index in binary, at a width the caller chooses. + +The codec need not be computable — a verifier's *decision* has to be +polynomial-time, but the correspondence between symbols and blocks is only used +to state what the decision means. + +## Main definitions + +- `Complexity.symEnc`, `Complexity.symDec` — the block of a symbol, and back + +## Main results + +- `Complexity.symDec_symEnc` — the codec round-trips +- `Complexity.length_symEnc` — blocks have the chosen width +-/ + +@[expose] public section + +namespace Complexity + +variable (α : Type) [Fintype α] [Inhabited α] + +/-- The block a symbol occupies: its index, in binary, at width `w`. -/ +noncomputable def symEnc (w : ℕ) (s : α) : List Bool := + bitsOfLenLE w (Fintype.equivFin α s).val + +/-- The symbol a block names. -/ +noncomputable def symDec (u : List Bool) : α := + if h : binValLE u < Fintype.card α then (Fintype.equivFin α).symm ⟨binValLE u, h⟩ + else default + +variable {α} + +omit [Inhabited α] in +@[simp] theorem length_symEnc (w : ℕ) (s : α) : (symEnc α w s).length = w := by + rw [symEnc, bitsOfLenLE_length] + +/-- **The codec round-trips**, as long as the width holds the alphabet. -/ +theorem symDec_symEnc {w : ℕ} (h : Fintype.card α ≤ 2 ^ w) (s : α) : + symDec α (symEnc α w s) = s := by + have hlt : (Fintype.equivFin α s).val < 2 ^ w := + lt_of_lt_of_le (Fintype.equivFin α s).isLt h + have hval : binValLE (symEnc α w s) = (Fintype.equivFin α s).val := by + rw [symEnc, binValLE_bitsOfLenLE _ _ hlt] + rw [symDec, hval, dif_pos (Fintype.equivFin α s).isLt] + simp + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/TesterChecks.lean b/Complexitylib/Classes/PCP/Internal/TesterChecks.lean new file mode 100644 index 00000000..5afe4f75 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/TesterChecks.lean @@ -0,0 +1,237 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.HadamardTester +public import Complexitylib.Classes.PCP.Internal.Arithmetize + +/-! +# The remaining checks of the assignment tester + +`HadamardTester` proves the linearity and consistency checks. Two more are +needed for the tester to plug into Dinur's composition, and both are analysed +here on raw tables, over bundled randomness, in the same style. + +* **The constraint check on a system.** The constraint is a *system* of + quadratic equations (the one-hot arithmetization of `Arithmetize`), checked + by one random linear combination. Passing on more than half of the + coefficient vectors — after paying for the reads — forces every equation to + hold on the decoded tables. + +* **The input check, one coordinate at a time.** The tester is handed its + input as a table it may read at a single random coordinate, and compares that + bit with the corresponding coordinate of its decoded assignment, obtained by + self-correction at a basis vector. Passing often means the input table is + *close* to the decoded assignment's input part — not equal, which a single + read could never certify, but close enough that, when the input is supposed + to be a Hadamard codeword, it decodes uniquely. + +The last point is what makes the composition work with constantly many +queries: the outer graph's labels are handed to the tester as Hadamard +codewords, and a wrong label is far from every right one. + +## Main results + +- `Complexity.forall_checkValue_of_prob_combined` — the random-combination + constraint check +- `Complexity.prob_coord_eq_ge` — the single-coordinate input check +- `Complexity.decodeLabel_eq` — a table close to a codeword decodes to it +-/ + +@[expose] public section + +namespace Complexity + +open BooleanAnalysis + +variable {n : ℕ} + +/-! ### The constraint check on a system -/ + +/-- **The random-combination constraint check.** Reading the combined constraint +off the raw tables passes often enough only if every constraint of the system +holds on the decoded tables. The coefficient vector is the first block, the +correction strings the second. -/ +theorem forall_checkValue_of_prob_combined {J : ℕ} (a : Cube n) (b : Cube (n * n)) + (C : Fin J → QuadConstraint n) + (f : BooleanFunction n) (hf : IsBooleanValued f) {ε : ℝ} + (hfc : IsClose f (signOf (hadamard a)) ε) + (g : BooleanFunction (n * n)) (hg : IsBooleanValued g) {ε' : ℝ} + (hgc : IsClose g (signOf (hadamard b)) ε') + (haccept : 1 / 2 + (2 * ε + 2 * ε') < Pr[fun z : Cube (J + (n + n * n)) => + ConstraintAccepts f g (QuadConstraint.combine C (leftBlock z)) (rightBlock z)]) : + ∀ j, checkValue (hadamard b) (hadamard a) (C j) = 0 := by + classical + have hgood : 1 - (2 * ε + 2 * ε') ≤ Pr[fun z : Cube (J + (n + n * n)) => + f (leftBlock (rightBlock z)) + * f ((QuadConstraint.combine C (leftBlock z)).lin + leftBlock (rightBlock z)) + = chi (hadamard a (QuadConstraint.combine C (leftBlock z)).lin) + ∧ g (rightBlock (rightBlock z)) + * g ((QuadConstraint.combine C (leftBlock z)).quad + rightBlock (rightBlock z)) + = chi (hadamard b (QuadConstraint.combine C (leftBlock z)).quad)] := + prob_blocks_ge (fun c w => + f (leftBlock w) * f ((QuadConstraint.combine C c).lin + leftBlock w) + = chi (hadamard a (QuadConstraint.combine C c).lin) + ∧ g (rightBlock w) * g ((QuadConstraint.combine C c).quad + rightBlock w) + = chi (hadamard b (QuadConstraint.combine C c).quad)) _ + fun c => prob_constraint_reads a b (QuadConstraint.combine C c) f hf hfc g hg hgc + have htrans := prob_le_of_imp_of_good + (E := fun z : Cube (J + (n + n * n)) => + ConstraintAccepts f g (QuadConstraint.combine C (leftBlock z)) (rightBlock z)) + (F := fun z : Cube (J + (n + n * n)) => + checkValue (hadamard b) (hadamard a) (QuadConstraint.combine C (leftBlock z)) = 0) + (A := fun z : Cube (J + (n + n * n)) => + f (leftBlock (rightBlock z)) + * f ((QuadConstraint.combine C (leftBlock z)).lin + leftBlock (rightBlock z)) + = chi (hadamard a (QuadConstraint.combine C (leftBlock z)).lin) + ∧ g (rightBlock (rightBlock z)) + * g ((QuadConstraint.combine C (leftBlock z)).quad + rightBlock (rightBlock z)) + = chi (hadamard b (QuadConstraint.combine C (leftBlock z)).quad)) + fun z hE hA => by + have h : signBit (chi (hadamard b (QuadConstraint.combine C (leftBlock z)).quad)) + + signBit (chi (hadamard a (QuadConstraint.combine C (leftBlock z)).lin)) + + (QuadConstraint.combine C (leftBlock z)).const = 0 := by + rw [← hA.1, ← hA.2] + exact hE + rw [signBit_chi, signBit_chi] at h + exact h + have hmarg : Pr[fun z : Cube (J + (n + n * n)) => + checkValue (hadamard b) (hadamard a) (QuadConstraint.combine C (leftBlock z)) = 0] + = Pr[fun c : Cube J => checkValue (hadamard b) (hadamard a) + (QuadConstraint.combine C c) = 0] := + prob_leftBlock (fun c : Cube J => + checkValue (hadamard b) (hadamard a) (QuadConstraint.combine C c) = 0) + rw [hmarg] at htrans + exact forall_checkValue_of_prob a b C (by linarith) + +/-! ### The input check, one coordinate at a time -/ + +/-- **The single-coordinate input check**, on the raw table: a random +coordinate `r` (first block) of the input table `w` is compared with the +decoded assignment's bit at variable `idx r`, read by self-correction at the +basis vector (correction string: second block). -/ +def CoordAccepts {m : ℕ} (f : BooleanFunction n) (w : Cube m → ZMod 2) (idx : Cube m → Fin n) + (z : Cube (m + n)) : Prop := + signBit (f (rightBlock z) * f (basisVec (idx (leftBlock z)) + rightBlock z)) + = w (leftBlock z) + +/-- **Passing the coordinate check means agreeing on most coordinates.** -/ +theorem prob_coord_eq_ge {m : ℕ} (a : Cube n) (f : BooleanFunction n) (hf : IsBooleanValued f) + {ε : ℝ} (hfc : IsClose f (signOf (hadamard a)) ε) + (w : Cube m → ZMod 2) (idx : Cube m → Fin n) : + Pr[CoordAccepts f w idx] - 2 * ε ≤ Pr[fun r : Cube m => a (idx r) = w r] := by + classical + have hgood : 1 - 2 * ε ≤ Pr[fun z : Cube (m + n) => + f (rightBlock z) * f (basisVec (idx (leftBlock z)) + rightBlock z) + = chi (hadamard a (basisVec (idx (leftBlock z))))] := + prob_blocks_ge (fun r c => + f c * f (basisVec (idx r) + c) = chi (hadamard a (basisVec (idx r)))) _ + fun r => prob_read_ge f hf a hfc (basisVec (idx r)) + have htrans := prob_le_of_imp_of_good (E := CoordAccepts f w idx) + (F := fun z : Cube (m + n) => a (idx (leftBlock z)) = w (leftBlock z)) + (A := fun z : Cube (m + n) => + f (rightBlock z) * f (basisVec (idx (leftBlock z)) + rightBlock z) + = chi (hadamard a (basisVec (idx (leftBlock z))))) + fun z hE hA => by + have h : signBit (chi (hadamard a (basisVec (idx (leftBlock z))))) = w (leftBlock z) := by + rw [← hA] + exact hE + rw [signBit_chi, hadamard_basisVec] at h + exact h + have hmarg : Pr[fun z : Cube (m + n) => a (idx (leftBlock z)) = w (leftBlock z)] + = Pr[fun r : Cube m => a (idx r) = w r] := + prob_leftBlock (fun r : Cube m => a (idx r) = w r) + rw [hmarg] at htrans + linarith + +/-- The honest table passes the coordinate check everywhere, when the input +table is the decoded assignment's input part. -/ +theorem coordAccepts_of_honest {m : ℕ} (a : Cube n) (idx : Cube m → Fin n) (z : Cube (m + n)) : + CoordAccepts (signOf (hadamard a)) (fun r => a (idx r)) idx z := by + show signBit (signOf (hadamard a) (rightBlock z) + * signOf (hadamard a) (basisVec (idx (leftBlock z)) + rightBlock z)) = _ + rw [corrected_read_honest, signBit_chi, hadamard_basisVec] + +/-! ### Decoding a label from a table -/ + +/-- The distance between two bit tables: the fraction of coordinates where they +differ. -/ +noncomputable def bitDist {m : ℕ} (s t : Cube m → ZMod 2) : ℝ := Pr[fun r => s r ≠ t r] + +theorem bitDist_eq_hammingDist {m : ℕ} (s t : Cube m → ZMod 2) : + bitDist s t = hammingDist (signOf s) (signOf t) := by + unfold bitDist hammingDist + congr 1 + funext r + show (s r ≠ t r) = (chi (s r) ≠ chi (t r)) + have hinj : ∀ u v : ZMod 2, chi u = chi v ↔ u = v := by + intro u v + constructor + · intro h + by_contra hne + have h' := signBit_chi u + rw [h, signBit_chi] at h' + exact hne h'.symm + · intro h + rw [h] + rw [ne_eq, ne_eq, hinj] + +theorem bitDist_comm {m : ℕ} (s t : Cube m → ZMod 2) : bitDist s t = bitDist t s := by + rw [bitDist_eq_hammingDist, bitDist_eq_hammingDist, hammingDist_comm] + +theorem bitDist_triangle {m : ℕ} (s t u : Cube m → ZMod 2) : + bitDist s u ≤ bitDist s t + bitDist t u := by + rw [bitDist_eq_hammingDist, bitDist_eq_hammingDist, bitDist_eq_hammingDist] + exact hammingDist_triangle _ _ _ + +theorem bitDist_eq_one_sub {m : ℕ} (s t : Cube m → ZMod 2) : + bitDist s t = 1 - Pr[fun r => s r = t r] := by + have := BooleanAnalysis.Internal.prob_compl (fun r : Cube m => s r = t r) + unfold bitDist + linarith + +/-- **Distinct codewords are far apart**: at distance exactly one half. -/ +theorem bitDist_hadamard {m : ℕ} (u v : Cube m) (h : u ≠ v) : + bitDist (hadamard u) (hadamard v) = 1 / 2 := by + have hne : u + v ≠ 0 := by + intro h0 + apply h + funext i + have hi := congrFun h0 i + rcases (by decide : ∀ x y : ZMod 2, x + y = 0 → x = y) (u i) (v i) hi with h' + exact h' + have hhalf := prob_hadamard_ne_zero (u + v) hne + unfold bitDist + rw [← hhalf] + congr 1 + funext r + rw [hadamard_add] + have hiff : ∀ x y : ZMod 2, (x ≠ y) ↔ (x + y ≠ 0) := by decide + exact propext (hiff _ _) + +/-- Decoding: the label whose codeword is within a quarter of the table, if +any; an arbitrary label otherwise. -/ +noncomputable def decodeLabel {m : ℕ} {β : Type} [Nonempty β] (enc : β → Cube m) + (t : Cube m → ZMod 2) : β := + open Classical in + if h : ∃ σ : β, bitDist t (hadamard (enc σ)) < 1 / 4 then Classical.choose h + else Classical.arbitrary β + +/-- **A table close to a codeword decodes to its label.** -/ +theorem decodeLabel_eq {m : ℕ} {β : Type} [Nonempty β] (enc : β → Cube m) + (henc : Function.Injective enc) (t : Cube m → ZMod 2) (σ : β) + (h : bitDist t (hadamard (enc σ)) < 1 / 4) : decodeLabel enc t = σ := by + classical + have hex : ∃ σ : β, bitDist t (hadamard (enc σ)) < 1 / 4 := ⟨σ, h⟩ + rw [decodeLabel, dif_pos hex] + have hspec := Classical.choose_spec hex + by_contra hne + have hne' : enc (Classical.choose hex) ≠ enc σ := fun heq => hne (henc heq) + have hfar := bitDist_hadamard _ _ hne' + have htri := bitDist_triangle (hadamard (enc (Classical.choose hex))) t (hadamard (enc σ)) + rw [bitDist_comm _ t] at htri + linarith + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/TesterCore.lean b/Complexitylib/Classes/PCP/Internal/TesterCore.lean new file mode 100644 index 00000000..7651c4d5 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/TesterCore.lean @@ -0,0 +1,294 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.TesterChecks + +/-! +# The assignment tester, assembled + +The six checks of the Hadamard assignment tester, run on one bundled random +string, on raw tables, with a universal soundness constant. + +The tester is handed two **input tables** `Tt Th : Cube B → ZMod 2` — in the +composition, the Hadamard encodings of the labels at the two ends of an outer +edge — and a **proof** consisting of a linear table `F` over `n` variables and +a quadratic table `G` over `n * n`, where the `n` variables are the `2 · 2^B` +input coordinates followed by the selectors of a one-hot system. Its random +string has six blocks, one per check: + +1. linearity of `F`; 2. linearity of `G`; 3. consistency of `G` with `F`; +4. the one-hot system, by a random linear combination; 5. and 6. the two +input tables, one coordinate each. + +Every check reads a constant number of positions — never a whole table — so +the total query count is a constant independent of `B`, and the soundness +constant `1/32` is universal. That independence is the point of the tester: +the outer alphabet may be enormous, and neither the query count nor the +soundness loss may notice. + +## Main definitions + +- `Complexity.Tester.AllChecks` — the tester's verdict on a random string + +## Main results + +- `Complexity.Tester.sound` — passing with probability above `31/32` yields an + assignment satisfying the system whose input parts are within `3/32` of the + input tables +- `Complexity.Tester.complete` — the honest proof of a satisfying assignment + passes on every random string +-/ + +@[expose] public section + +namespace Complexity + +open BooleanAnalysis + +namespace Tester + +/-! ### Layout -/ + +/-- The number of input variables: two tables of `2^B` coordinates. -/ +abbrev kOf (B : ℕ) : ℕ := 2 ^ B + 2 ^ B + +/-- The number of variables: input coordinates and one-hot selectors. -/ +abbrev nOf (B : ℕ) : ℕ := kOf B + 2 ^ kOf B + +/-- The number of constraints in the one-hot system. -/ +abbrev JOf (B : ℕ) : ℕ := Fintype.card (OneHotIdx (kOf B)) + +/-- The variable holding coordinate `r` of the first input table. -/ +noncomputable def inTail (B : ℕ) (r : Cube B) : Fin (nOf B) := + Fin.castAdd (2 ^ kOf B) (Fin.castAdd (2 ^ B) (candIdx B r)) + +/-- The variable holding coordinate `r` of the second input table. -/ +noncomputable def inHead (B : ℕ) (r : Cube B) : Fin (nOf B) := + Fin.castAdd (2 ^ kOf B) (Fin.natAdd (2 ^ B) (candIdx B r)) + +/-- The first input table an assignment of the variables carries. -/ +noncomputable def tailPart {B : ℕ} (a : Cube (nOf B)) : Cube B → ZMod 2 := + fun r => a (inTail B r) + +/-- The second input table an assignment of the variables carries. -/ +noncomputable def headPart {B : ℕ} (a : Cube (nOf B)) : Cube B → ZMod 2 := + fun r => a (inHead B r) + +/-- The random bits of the linearity check on `F`. -/ +abbrev R1 (B : ℕ) : ℕ := nOf B + nOf B +/-- The random bits of the linearity check on `G`. -/ +abbrev R2 (B : ℕ) : ℕ := nOf B * nOf B + nOf B * nOf B +/-- The random bits of the consistency check. -/ +abbrev R3 (B : ℕ) : ℕ := (nOf B + nOf B) + (nOf B + (nOf B + nOf B * nOf B)) +/-- The random bits of the constraint check. -/ +abbrev R4 (B : ℕ) : ℕ := JOf B + (nOf B + nOf B * nOf B) +/-- The random bits of one input check. -/ +abbrev R5 (B : ℕ) : ℕ := B + nOf B + +/-- The random bits of the last two blocks. -/ +abbrev Rest5 (B : ℕ) : ℕ := R5 B + R5 B +/-- The random bits of the last three blocks. -/ +abbrev Rest4 (B : ℕ) : ℕ := R4 B + Rest5 B +/-- The random bits of the last four blocks. -/ +abbrev Rest3 (B : ℕ) : ℕ := R3 B + Rest4 B +/-- The random bits after the first block. -/ +abbrev Rest2 (B : ℕ) : ℕ := R2 B + Rest3 B + +/-- The total number of random bits. -/ +abbrev ROf (B : ℕ) : ℕ := R1 B + Rest2 B + +variable {B : ℕ} + +/-- The first block of the random string: linearity of `F`. -/ +def blk1 (z : Cube (ROf B)) : Cube (R1 B) := leftBlock z +/-- The second block: linearity of `G`. -/ +def blk2 (z : Cube (ROf B)) : Cube (R2 B) := leftBlock (rightBlock z) +/-- The third block: consistency. -/ +def blk3 (z : Cube (ROf B)) : Cube (R3 B) := leftBlock (rightBlock (rightBlock z)) +/-- The fourth block: the constraint system. -/ +def blk4 (z : Cube (ROf B)) : Cube (R4 B) := leftBlock (rightBlock (rightBlock (rightBlock z))) +/-- The fifth block: the first input table. -/ +def blk5 (z : Cube (ROf B)) : Cube (R5 B) := + leftBlock (rightBlock (rightBlock (rightBlock (rightBlock z)))) +/-- The sixth block: the second input table. -/ +def blk6 (z : Cube (ROf B)) : Cube (R5 B) := + rightBlock (rightBlock (rightBlock (rightBlock (rightBlock z)))) + +/-! ### Each block is uniform -/ + +theorem prob_blk1 (P : Cube (R1 B) → Prop) : Pr[fun z : Cube (ROf B) => P (blk1 z)] = Pr[P] := + prob_leftBlock P + +theorem prob_blk2 (P : Cube (R2 B) → Prop) : Pr[fun z : Cube (ROf B) => P (blk2 z)] = Pr[P] := by + have h1 := prob_rightBlock (a := R1 B) (fun w : Cube (Rest2 B) => P (leftBlock w)) + rw [show (fun z : Cube (ROf B) => P (blk2 z)) = fun z => P (leftBlock (rightBlock z)) from rfl, + h1] + exact prob_leftBlock P + +theorem prob_blk3 (P : Cube (R3 B) → Prop) : Pr[fun z : Cube (ROf B) => P (blk3 z)] = Pr[P] := by + have h1 := prob_rightBlock (a := R1 B) + (fun w : Cube (Rest2 B) => P (leftBlock (rightBlock w))) + have h2 := prob_rightBlock (a := R2 B) (fun w : Cube (Rest3 B) => P (leftBlock w)) + rw [show (fun z : Cube (ROf B) => P (blk3 z)) + = fun z => P (leftBlock (rightBlock (rightBlock z))) from rfl, h1, h2] + exact prob_leftBlock P + +theorem prob_blk4 (P : Cube (R4 B) → Prop) : Pr[fun z : Cube (ROf B) => P (blk4 z)] = Pr[P] := by + have h1 := prob_rightBlock (a := R1 B) + (fun w : Cube (Rest2 B) => P (leftBlock (rightBlock (rightBlock w)))) + have h2 := prob_rightBlock (a := R2 B) + (fun w : Cube (Rest3 B) => P (leftBlock (rightBlock w))) + have h3 := prob_rightBlock (a := R3 B) (fun w : Cube (Rest4 B) => P (leftBlock w)) + rw [show (fun z : Cube (ROf B) => P (blk4 z)) + = fun z => P (leftBlock (rightBlock (rightBlock (rightBlock z)))) from rfl, h1, h2, h3] + exact prob_leftBlock P + +theorem prob_blk5 (P : Cube (R5 B) → Prop) : Pr[fun z : Cube (ROf B) => P (blk5 z)] = Pr[P] := by + have h1 := prob_rightBlock (a := R1 B) + (fun w : Cube (Rest2 B) => P (leftBlock (rightBlock (rightBlock (rightBlock w))))) + have h2 := prob_rightBlock (a := R2 B) + (fun w : Cube (Rest3 B) => P (leftBlock (rightBlock (rightBlock w)))) + have h3 := prob_rightBlock (a := R3 B) + (fun w : Cube (Rest4 B) => P (leftBlock (rightBlock w))) + have h4 := prob_rightBlock (a := R4 B) (fun w : Cube (Rest5 B) => P (leftBlock w)) + rw [show (fun z : Cube (ROf B) => P (blk5 z)) + = fun z => P (leftBlock (rightBlock (rightBlock (rightBlock (rightBlock z))))) from rfl, + h1, h2, h3, h4] + exact prob_leftBlock P + +theorem prob_blk6 (P : Cube (R5 B) → Prop) : Pr[fun z : Cube (ROf B) => P (blk6 z)] = Pr[P] := by + have h1 := prob_rightBlock (a := R1 B) + (fun w : Cube (Rest2 B) => P (rightBlock (rightBlock (rightBlock (rightBlock w))))) + have h2 := prob_rightBlock (a := R2 B) + (fun w : Cube (Rest3 B) => P (rightBlock (rightBlock (rightBlock w)))) + have h3 := prob_rightBlock (a := R3 B) + (fun w : Cube (Rest4 B) => P (rightBlock (rightBlock w))) + have h4 := prob_rightBlock (a := R4 B) (fun w : Cube (Rest5 B) => P (rightBlock w)) + rw [show (fun z : Cube (ROf B) => P (blk6 z)) + = fun z => P (rightBlock (rightBlock (rightBlock (rightBlock (rightBlock z))))) from rfl, + h1, h2, h3, h4] + exact prob_rightBlock P + +/-! ### The checks -/ + +/-- The linearity check on a table, reading at the two halves of the block and +their sum. -/ +def LinCheck {m : ℕ} (f : BooleanFunction m) (x : Cube (m + m)) : Prop := + f (leftBlock x) * f (rightBlock x) = f (leftBlock x + rightBlock x) + +theorem prob_linCheck {m : ℕ} (f : BooleanFunction m) : + Pr[LinCheck f] = blrAcceptProb f := by + unfold blrAcceptProb + rw [prob₂_eq_prob_blocks] + rfl + +/-- **The tester's verdict**: all six checks pass on the random string. -/ +def AllChecks (S : Finset (Cube (kOf B))) (Tt Th : Cube B → ZMod 2) + (F : Cube (nOf B) → ZMod 2) (G : Cube (nOf B * nOf B) → ZMod 2) (z : Cube (ROf B)) : Prop := + LinCheck (signOf F) (blk1 z) + ∧ LinCheck (signOf G) (blk2 z) + ∧ TesterAccepts (signOf F) (signOf G) (blk3 z) + ∧ ConstraintAccepts (signOf F) (signOf G) + (QuadConstraint.combine (oneHotSystem S) (leftBlock (blk4 z))) (rightBlock (blk4 z)) + ∧ CoordAccepts (signOf F) Tt (inTail B) (blk5 z) + ∧ CoordAccepts (signOf F) Th (inHead B) (blk6 z) + +theorem isBooleanValued_signOf {m : ℕ} (F : Cube m → ZMod 2) : IsBooleanValued (signOf F) := by + intro x + show chi (F x) = 1 ∨ chi (F x) = -1 + rcases (by decide : ∀ u : ZMod 2, u = 0 ∨ u = 1) (F x) with h | h <;> rw [h] <;> simp [chi] + +/-! ### Soundness -/ + +/-- **Soundness of the assembled tester.** If the six checks all pass with +probability above `31/32`, the proof decodes to an assignment of the variables +that satisfies the one-hot system and whose two input parts are within `3/32` +of the input tables. -/ +theorem sound (S : Finset (Cube (kOf B))) (Tt Th : Cube B → ZMod 2) + (F : Cube (nOf B) → ZMod 2) (G : Cube (nOf B * nOf B) → ZMod 2) + (h : 1 - 1 / 32 < Pr[AllChecks S Tt Th F G]) : + ∃ a : Cube (nOf B), (∀ j, (oneHotSystem S j).Sat a) + ∧ bitDist Tt (tailPart a) ≤ 3 / 32 ∧ bitDist Th (headPart a) ≤ 3 / 32 := by + classical + have hf := isBooleanValued_signOf F + have hg := isBooleanValued_signOf G + -- each check passes with probability above 31/32 + have h1 : 1 - 1 / 32 < Pr[LinCheck (signOf F)] := by + rw [← prob_blk1 (LinCheck (signOf F))] + exact lt_of_lt_of_le h (prob_mono fun z hz => hz.1) + have h2 : 1 - 1 / 32 < Pr[LinCheck (signOf G)] := by + rw [← prob_blk2 (LinCheck (signOf G))] + exact lt_of_lt_of_le h (prob_mono fun z hz => hz.2.1) + have h3 : 1 - 1 / 32 < Pr[TesterAccepts (signOf F) (signOf G)] := by + rw [← prob_blk3 (TesterAccepts (signOf F) (signOf G))] + exact lt_of_lt_of_le h (prob_mono fun z hz => hz.2.2.1) + have h4 : 1 - 1 / 32 < Pr[fun x : Cube (R4 B) => ConstraintAccepts (signOf F) (signOf G) + (QuadConstraint.combine (oneHotSystem S) (leftBlock x)) (rightBlock x)] := by + rw [← prob_blk4 (fun x : Cube (R4 B) => ConstraintAccepts (signOf F) (signOf G) + (QuadConstraint.combine (oneHotSystem S) (leftBlock x)) (rightBlock x))] + exact lt_of_lt_of_le h (prob_mono fun z hz => hz.2.2.2.1) + have h5 : 1 - 1 / 32 < Pr[CoordAccepts (signOf F) Tt (inTail B)] := by + rw [← prob_blk5 (CoordAccepts (signOf F) Tt (inTail B))] + exact lt_of_lt_of_le h (prob_mono fun z hz => hz.2.2.2.2.1) + have h6 : 1 - 1 / 32 < Pr[CoordAccepts (signOf F) Th (inHead B)] := by + rw [← prob_blk6 (CoordAccepts (signOf F) Th (inHead B))] + exact lt_of_lt_of_le h (prob_mono fun z hz => hz.2.2.2.2.2) + -- decode + rw [prob_linCheck] at h1 h2 + obtain ⟨a, ha⟩ := exists_assignment_of_blr (signOf F) hf (1 / 32) (le_of_lt h1) + obtain ⟨b, hb⟩ := exists_assignment_of_blr (signOf G) hg (1 / 32) (le_of_lt h2) + have hfc : IsClose (signOf F) (signOf (hadamard a)) (1 / 32) := ha + have hgc : IsClose (signOf G) (signOf (hadamard b)) (1 / 32) := hb + have hb' : b = tensorAssign a := + eq_tensorAssign_of_prob_tester a b (signOf F) hf hfc (signOf G) hg hgc (by linarith) + have hsys := forall_checkValue_of_prob_combined a b (oneHotSystem S) (signOf F) hf hfc + (signOf G) hg hgc (by linarith) + refine ⟨a, fun j => ?_, ?_, ?_⟩ + · have := hsys j + rw [hb'] at this + exact this + · have hc := prob_coord_eq_ge a (signOf F) hf hfc Tt (inTail B) + rw [bitDist_comm, bitDist_eq_one_sub] + show 1 - Pr[fun r : Cube B => a (inTail B r) = Tt r] ≤ 3 / 32 + linarith + · have hc := prob_coord_eq_ge a (signOf F) hf hfc Th (inHead B) + rw [bitDist_comm, bitDist_eq_one_sub] + show 1 - Pr[fun r : Cube B => a (inHead B r) = Th r] ≤ 3 / 32 + linarith + +/-! ### Completeness -/ + +theorem linCheck_hadamard {m : ℕ} (a : Cube m) (x : Cube (m + m)) : + LinCheck (signOf (hadamard a)) x := by + show chi (hadamard a (leftBlock x)) * chi (hadamard a (rightBlock x)) + = chi (hadamard a (leftBlock x + rightBlock x)) + rw [hadamard_add_arg, BooleanAnalysis.Internal.chi_add] + +/-- The honest tables pass the constraint check on every random string. -/ +theorem constraintAccepts_of_honest {m : ℕ} (a : Cube m) (C : QuadConstraint m) (hC : C.Sat a) + (z : Cube (m + m * m)) : + ConstraintAccepts (signOf (hadamard a)) (signOf (hadamard (tensorAssign a))) C z := by + show signBit (signOf (hadamard (tensorAssign a)) (rightBlock z) + * signOf (hadamard (tensorAssign a)) (C.quad + rightBlock z)) + + signBit (signOf (hadamard a) (leftBlock z) * signOf (hadamard a) (C.lin + leftBlock z)) + + C.const = 0 + rw [corrected_read_honest, corrected_read_honest, signBit_chi, signBit_chi] + exact hC + +/-- **Completeness of the assembled tester.** The honest proof of an assignment +satisfying the system — its Hadamard encoding and that of its tensor square, +with the input tables read off the assignment — passes every check on every +random string. -/ +theorem complete (S : Finset (Cube (kOf B))) (a : Cube (nOf B)) + (ha : ∀ j, (oneHotSystem S j).Sat a) (z : Cube (ROf B)) : + AllChecks S (tailPart a) (headPart a) (hadamard a) (hadamard (tensorAssign a)) z := + ⟨linCheck_hadamard a _, linCheck_hadamard (tensorAssign a) _, testerAccepts_of_honest a _, + constraintAccepts_of_honest a _ (QuadConstraint.sat_combine (oneHotSystem S) a ha _) _, + coordAccepts_of_honest a (inTail B) _, coordAccepts_of_honest a (inHead B) _⟩ + +end Tester + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/ThreeSATReduction.lean b/Complexitylib/Classes/PCP/Internal/ThreeSATReduction.lean new file mode 100644 index 00000000..956d0ef9 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/ThreeSATReduction.lean @@ -0,0 +1,273 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.ConstraintGraph +public import Complexitylib.SAT.ThreeCNF + +/-! +# From 3CNF-SAT to binary constraint graphs + +The standard reduction turning a 3CNF formula into a constraint graph over the +alphabet `Fin 3 → Bool`, together with its correctness proof. This is the entry +point of Dinur's proof of the PCP theorem: it produces the constraint graph +whose unsatisfiability value the amplification step then boosts. + +## The construction + +Given `φ : CNF`, the graph `toGraph φ` has + +* one **variable vertex** for each variable index `0 … φ.maxVar`, and +* one **clause vertex** for each clause of `φ`, numbered `(φ.maxVar + 1) + j`; +* one edge for each (clause, position) pair, so `3 * φ.length` edges in all. + +A variable vertex is meant to carry the value of its variable in bit `0` of its +label; a clause vertex is meant to carry the values of the three variables its +clause mentions, one per coordinate. The edge for clause `j` and position `p` +checks both that the clause vertex's triple satisfies clause `j` and that its +`p`-th coordinate agrees with the `p`-th variable vertex — the usual +consistency-plus-satisfaction pair of constraints. + +## Main definitions + +- `litOf` — a total lookup of the literal at a given clause and position +- `numVerts`, `numEdges`, `varVertex`, `clauseVertex`, `edgeClause`, `edgePos` +- `clauseSat` — whether a triple of bits satisfies a clause +- `toGraph` — the constraint graph produced by the reduction +- `vertexLabel`, `mkAssign` — the two translations between assignments + +## Main results + +- `numVerts_toGraph`, `numEdges_toGraph` — the size of the produced graph +- `satisfiable_toGraph_iff` — correctness of the reduction on 3CNF inputs +-/ + +@[expose] public section + +namespace Complexity + +namespace ThreeSATCSP + +open SAT + +/-! ### Indexing helpers -/ + +/-- The literal at position `p` of clause `j` of `φ`, defaulting to the positive +literal on variable `0` when either index is out of range. Totality keeps the +reduction free of dependent-index bookkeeping. -/ +def litOf (φ : CNF) (j : ℕ) (p : Fin 3) : Lit := + ((φ[j]?).getD []).getD p.val ⟨false, 0⟩ + +/-- The number of vertices of the constraint graph of `φ`: one per variable +index `0 … φ.maxVar`, then one per clause. -/ +def numVerts (φ : CNF) : ℕ := (φ.maxVar + 1) + φ.length + +/-- The number of edges of the constraint graph of `φ`: three per clause. -/ +def numEdges (φ : CNF) : ℕ := 3 * φ.length + +/-- The constraint graph of `φ` always has at least one vertex, namely the +variable vertex `0`. -/ +theorem numVerts_pos (φ : CNF) : 0 < numVerts φ := by + unfold numVerts; omega + +/-- The vertex carrying the value of variable `v`; out-of-range indices are +folded onto vertex `0`. -/ +def varVertex (φ : CNF) (v : ℕ) : Fin (numVerts φ) := + if h : v < numVerts φ then ⟨v, h⟩ else ⟨0, numVerts_pos φ⟩ + +/-- The vertex carrying the claimed values of the variables of clause `j`; +out-of-range indices are folded onto vertex `0`. -/ +def clauseVertex (φ : CNF) (j : ℕ) : Fin (numVerts φ) := + if h : (φ.maxVar + 1) + j < numVerts φ then ⟨(φ.maxVar + 1) + j, h⟩ + else ⟨0, numVerts_pos φ⟩ + +/-- The clause that edge number `e` belongs to. -/ +def edgeClause (e : ℕ) : ℕ := e / 3 + +/-- The position inside its clause that edge number `e` checks. -/ +def edgePos (e : ℕ) : Fin 3 := ⟨e % 3, Nat.mod_lt _ (by omega)⟩ + +/-- Whether the triple `cl` of claimed variable values satisfies clause `j`: +some position's claimed value matches that literal's sign. -/ +def clauseSat (φ : CNF) (j : ℕ) (cl : Fin 3 → Bool) : Bool := + decide (∃ q : Fin 3, cl q = (litOf φ j q).sign) + +/-- `clauseSat` reflects the existential it decides. -/ +theorem clauseSat_eq_true_iff {φ : CNF} {j : ℕ} {cl : Fin 3 → Bool} : + clauseSat φ j cl = true ↔ ∃ q : Fin 3, cl q = (litOf φ j q).sign := by + simp [clauseSat] + +/-! ### The reduction -/ + +/-- The constraint graph of a 3CNF formula `φ`. Variable vertices come first, +clause vertices after them; edge `e` links clause vertex `edgeClause e` to the +variable vertex of the literal at position `edgePos e` of that clause, and its +constraint demands both that the clause vertex's triple satisfies the clause and +that it agrees with the variable vertex on bit `0`. -/ +def toGraph (φ : CNF) : ConstraintGraph (Fin 3 → Bool) where + numVerts := numVerts φ + numEdges := numEdges φ + tail e := clauseVertex φ (edgeClause e.val) + head e := varVertex φ (litOf φ (edgeClause e.val) (edgePos e.val)).var + rel e cl va := clauseSat φ (edgeClause e.val) cl && (cl (edgePos e.val) == va 0) + +/-- The reduction produces `3 * φ.length` edges. -/ +theorem numEdges_toGraph (φ : CNF) : (toGraph φ).numEdges = 3 * φ.length := rfl + +/-- The reduction produces `(φ.maxVar + 1) + φ.length` vertices. -/ +theorem numVerts_toGraph (φ : CNF) : + (toGraph φ).numVerts = (φ.maxVar + 1) + φ.length := rfl + +/-! ### Basic facts about the indexing helpers -/ + +/-- A variable vertex within range keeps its index. -/ +theorem varVertex_val {φ : CNF} {v : ℕ} (hv : v ≤ φ.maxVar) : + (varVertex φ v).val = v := by + have h : v < numVerts φ := by unfold numVerts; omega + simp [varVertex, h] + +/-- A clause vertex of an existing clause sits at index `(φ.maxVar + 1) + j`. -/ +theorem clauseVertex_val {φ : CNF} {j : ℕ} (hj : j < φ.length) : + (clauseVertex φ j).val = (φ.maxVar + 1) + j := by + have h : (φ.maxVar + 1) + j < numVerts φ := by unfold numVerts; omega + simp [clauseVertex, h] + +/-- `litOf` really reads the list entry it is meant to read. -/ +theorem litOf_eq {φ : CNF} {j : ℕ} {c : Clause} (hc : φ[j]? = some c) {p : Fin 3} + (hp : p.val < c.length) : litOf φ j p = c[p.val] := by + rw [litOf, hc, Option.getD_some, List.getD_eq_getElem?_getD, + List.getElem?_eq_getElem hp, Option.getD_some] + +/-- Every literal produced by `litOf` at an existing clause really occurs in +that clause, provided the clause has three literals. -/ +theorem litOf_mem {φ : CNF} {j : ℕ} {c : Clause} (hc : φ[j]? = some c) + (hlen : c.length = 3) (p : Fin 3) : litOf φ j p ∈ c := by + have hp : p.val < c.length := by omega + rw [litOf_eq hc hp] + exact List.getElem_mem hp + +/-- Every edge belongs to a clause that exists. -/ +theorem edgeClause_lt {φ : CNF} (e : Fin (toGraph φ).numEdges) : + edgeClause e.val < φ.length := by + have h : e.val < 3 * φ.length := e.isLt + unfold edgeClause + omega + +/-- Every (clause, position) pair is realized by an edge. -/ +theorem exists_edge {φ : CNF} {j : ℕ} (hj : j < φ.length) (q : Fin 3) : + ∃ e : Fin (toGraph φ).numEdges, edgeClause e.val = j ∧ edgePos e.val = q := by + refine ⟨⟨3 * j + q.val, ?_⟩, ?_, ?_⟩ + · show 3 * j + q.val < 3 * φ.length; omega + · show (3 * j + q.val) / 3 = j; omega + · apply Fin.ext; show (3 * j + q.val) % 3 = q.val; omega + +/-- The edge constraint of `toGraph`, spelled out. -/ +theorem satisfies_iff {φ : CNF} {a : (toGraph φ).Assignment} + (e : Fin (toGraph φ).numEdges) : + (toGraph φ).Satisfies a e ↔ + (clauseSat φ (edgeClause e.val) (a (clauseVertex φ (edgeClause e.val))) = true ∧ + a (clauseVertex φ (edgeClause e.val)) (edgePos e.val) + = a (varVertex φ (litOf φ (edgeClause e.val) (edgePos e.val)).var) 0) := by + simp [ConstraintGraph.Satisfies, ConstraintGraph.satisfies, toGraph] + +/-- Every literal of a 3CNF formula mentions a variable at most `φ.maxVar`. -/ +theorem var_litOf_le_maxVar {φ : CNF} (h3 : φ.Is3CNF) {j : ℕ} (hj : j < φ.length) + (p : Fin 3) : (litOf φ j p).var ≤ φ.maxVar := by + have hmem : φ[j] ∈ φ := List.getElem_mem hj + have hc : φ[j]? = some φ[j] := List.getElem?_eq_getElem hj + have hlen : (φ[j]).length = 3 := h3 _ hmem + calc (litOf φ j p).var ≤ (φ[j]).maxVar := + Clause.var_le_maxVar (litOf_mem hc hlen p) + _ ≤ φ.maxVar := CNF.clause_maxVar_le_maxVar hmem + +/-! ### Translating assignments -/ + +/-- The label the graph assignment induced by a CNF assignment `α` puts on +vertex number `w`: a variable vertex gets the constant value of its variable, a +clause vertex gets the values of the three variables its clause mentions. -/ +def vertexLabel (φ : CNF) (α : SAT.Assignment) (w : ℕ) : Fin 3 → Bool := + if w < φ.maxVar + 1 then (fun _ => Assignment.get α w) + else (fun q => Assignment.get α (litOf φ (w - (φ.maxVar + 1)) q).var) + +/-- The label of a variable vertex. -/ +theorem vertexLabel_var {φ : CNF} {α : SAT.Assignment} {v : ℕ} (hv : v ≤ φ.maxVar) : + vertexLabel φ α v = fun _ => Assignment.get α v := by + have h : v < φ.maxVar + 1 := by omega + simp [vertexLabel, h] + +/-- The label of a clause vertex. -/ +theorem vertexLabel_clause {φ : CNF} {α : SAT.Assignment} {j : ℕ} : + vertexLabel φ α ((φ.maxVar + 1) + j) + = fun q => Assignment.get α (litOf φ j q).var := by + have h : ¬ ((φ.maxVar + 1) + j < φ.maxVar + 1) := by omega + simp [vertexLabel, h] + +/-- The CNF assignment read off from a graph assignment: variable `v` takes the +value of bit `0` of the label of its variable vertex. -/ +def mkAssign (φ : CNF) (a : (toGraph φ).Assignment) : SAT.Assignment := + (List.range (φ.maxVar + 1)).map (fun v => a (varVertex φ v) 0) + +/-- `mkAssign` reads back the label bit it was built from. -/ +theorem get_mkAssign {φ : CNF} {a : (toGraph φ).Assignment} {v : ℕ} + (hv : v ≤ φ.maxVar) : Assignment.get (mkAssign φ a) v = a (varVertex φ v) 0 := by + have hr : v < (List.range (φ.maxVar + 1)).length := by simp; omega + simp [Assignment.get, mkAssign, List.getElem?_map, + List.getElem?_eq_getElem hr] + +/-! ### Correctness -/ + +/-- **Correctness of the reduction.** For a 3CNF formula, the constraint graph +produced by `toGraph` is satisfiable exactly when the formula is. -/ +theorem satisfiable_toGraph_iff {φ : CNF} (h3 : φ.Is3CNF) : + (toGraph φ).Satisfiable ↔ φ.Satisfiable := by + constructor + · rintro ⟨a, ha⟩ + refine ⟨mkAssign φ a, ?_⟩ + rw [CNF.eval, List.all_eq_true] + intro c hcmem + obtain ⟨j, hj, rfl⟩ := List.getElem_of_mem hcmem + obtain ⟨e0, hc0, -⟩ := exists_edge (φ := φ) hj 0 + have h0 := (satisfies_iff e0).mp (ha e0) + rw [hc0] at h0 + obtain ⟨q, hq⟩ := clauseSat_eq_true_iff.mp h0.1 + obtain ⟨e1, hc1, hp1⟩ := exists_edge (φ := φ) hj q + have h1 := (satisfies_iff e1).mp (ha e1) + rw [hc1, hp1] at h1 + have hvar : (litOf φ j q).var ≤ φ.maxVar := var_litOf_le_maxVar h3 hj q + have hval : Assignment.get (mkAssign φ a) (litOf φ j q).var = (litOf φ j q).sign := by + rw [get_mkAssign hvar, ← h1.2, hq] + rw [Clause.eval, List.any_eq_true] + refine ⟨litOf φ j q, litOf_mem (List.getElem?_eq_getElem hj) (h3 _ hcmem) q, ?_⟩ + simp [Lit.eval, hval] + · rintro ⟨α, hα⟩ + refine ⟨fun w => vertexLabel φ α w.val, ?_⟩ + intro e + have hj : edgeClause e.val < φ.length := edgeClause_lt e + rw [satisfies_iff] + set j := edgeClause e.val with hjd + set p := edgePos e.val with hpd + have hvar : (litOf φ j p).var ≤ φ.maxVar := var_litOf_le_maxVar h3 hj p + have hcl : vertexLabel φ α (clauseVertex φ j).val + = fun q => Assignment.get α (litOf φ j q).var := by + rw [clauseVertex_val hj]; exact vertexLabel_clause + have hhead : vertexLabel φ α (varVertex φ (litOf φ j p).var).val + = fun _ => Assignment.get α (litOf φ j p).var := by + rw [varVertex_val hvar]; exact vertexLabel_var hvar + simp only [hcl, hhead] + refine ⟨clauseSat_eq_true_iff.mpr ?_, trivial⟩ + have hlen : (φ[j]).length = 3 := h3 _ (List.getElem_mem hj) + have hcls : Clause.eval α φ[j] = true := by + rw [CNF.eval, List.all_eq_true] at hα + exact hα _ (List.getElem_mem hj) + rw [Clause.eval, List.any_eq_true] at hcls + obtain ⟨ℓ, hℓmem, hℓ⟩ := hcls + obtain ⟨i, hi, rfl⟩ := List.getElem_of_mem hℓmem + refine ⟨⟨i, by omega⟩, ?_⟩ + rw [litOf_eq (p := ⟨i, by omega⟩) (List.getElem?_eq_getElem hj) hi] + simpa [Lit.eval] using hℓ + +end ThreeSATCSP + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/TowerFin.lean b/Complexitylib/Classes/PCP/Internal/TowerFin.lean new file mode 100644 index 00000000..092d5bee --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/TowerFin.lean @@ -0,0 +1,476 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.ZigZagBaseExists + +/-! +# The zig-zag tower, numbered + +`ZigZagTower` builds its members out of nested product types and names the +base's vertices by an arbitrary bijection. That is enough for the mathematics, +but an algorithm has to be handed numbers. This module rebuilds the tower with +every vertex and dart numbered, and with each naming chosen explicitly: pairs +are packed by `finProdFinEquiv`, so a level-`k` vertex is a mixed-radix numeral +with `k + 1` digits in base `deg ^ 4`. + +The recursion carries its own involutivity proof, which is what lets the next +level's types be `Fin` on the nose rather than up to a transport. + +## Main definitions + +- `Complexity.FinBase` — a numbered zig-zag base +- `Complexity.FinBase.data` — the rotation map at each level, with its proof +- `Complexity.FinBase.graphAt` — the level's graph +- `Complexity.FinBase.rotVal` — the same rotation map on raw numbers + +## Main results + +- `Complexity.FinBase.graphAt_succ` — one level is a zig-zag of the previous +- `Complexity.FinBase.spectral_graphAt` — every level has bound `2 / 5` +- `Complexity.FinBase.rotVal_eq` — the numeric recursion computes it +-/ + +@[expose] public section + +namespace Complexity + +/-! ### The recursion, in the open -/ + +namespace RegGraph + +/-- The rotation map of the square: walk two darts, and hand back the two +return labels in the opposite order. -/ +theorem rot_power_two (G : RegGraph) (v : G.V) (s : Fin 2 → G.D) : + (G.power 2).rot (v, s) = + ((G.rot ((G.rot (v, s 0)).1, s 1)).1, + ![(G.rot ((G.rot (v, s 0)).1, s 1)).2, (G.rot (v, s 0)).2]) := by + have hw1 : G.walkAt 2 v s 1 = (G.rot (v, s 0)).1 := by + rw [G.walkAt_succ_of_lt v s (by norm_num : (0 : ℕ) < 2)] + rfl + have hw2 : G.walkAt 2 v s 2 = (G.rot ((G.rot (v, s 0)).1, s 1)).1 := by + rw [G.walkAt_succ_of_lt v s (by norm_num : (1 : ℕ) < 2), hw1] + rfl + refine Prod.ext ?_ ?_ + · show G.walkEnd 2 v s = _ + rw [← G.walkAt_self_eq_walkEnd] + exact hw2 + · show G.revWalk v s = _ + funext j + fin_cases j + · show G.backLabel v s (Fin.rev 0) = _ + show (G.rot (G.walkAt 2 v s (Fin.rev (0 : Fin 2)).val, s (Fin.rev 0))).2 = _ + norm_num [hw1] + rfl + · show G.backLabel v s (Fin.rev 1) = _ + show (G.rot (G.walkAt 2 v s (Fin.rev (1 : Fin 2)).val, s (Fin.rev 1))).2 = _ + rfl + +end RegGraph + +/-- A zig-zag base with its vertices and darts numbered: `deg ^ 4` vertices, +`deg` darts, and a spectral bound of a fifth. -/ +structure FinBase where + /-- The degree. -/ + deg : ℕ + /-- The degree is positive. -/ + deg_pos : 0 < deg + /-- The rotation map. -/ + rot : Fin (deg ^ 4) × Fin deg → Fin (deg ^ 4) × Fin deg + /-- It is an involution. -/ + rot_involutive : Function.Involutive rot + /-- The spectral bound. -/ + lam : ℝ + /-- It is nonnegative. -/ + lam_nonneg : 0 ≤ lam + /-- And at most a fifth. -/ + lam_le : lam ≤ 1 / 5 + /-- The bound holds. -/ + spectral : (RegGraph.ofRot deg deg_pos (deg ^ 4) rot rot_involutive).SpectralBound lam + +namespace FinBase + +variable (F : FinBase) + +/-- The base graph. -/ +def graph : RegGraph := RegGraph.ofRot F.deg F.deg_pos (F.deg ^ 4) F.rot F.rot_involutive + +@[simp] theorem V_graph : F.graph.V = Fin (F.deg ^ 4) := rfl + +@[simp] theorem D_graph : F.graph.D = Fin F.deg := rfl + +@[simp] theorem deg_graph : F.graph.deg = F.deg := Fintype.card_fin _ + +theorem sq_pos : 0 < F.deg ^ 2 := pow_pos F.deg_pos 2 + +/-- The number of vertices at level `k`. -/ +def size (k : ℕ) : ℕ := (F.deg ^ 4) ^ (k + 1) + +theorem size_succ (k : ℕ) : F.size (k + 1) = F.size k * F.deg ^ 4 := by + rw [size, size, pow_succ] + +/-- **The numbered base is a zig-zag base.** -/ +def toBase : ZigZagBase where + base := F.graph + card_eq := by + show Fintype.card (Fin (F.deg ^ 4)) = F.graph.deg ^ 4 + rw [Fintype.card_fin, deg_graph] + lam := F.lam + lam_nonneg := F.lam_nonneg + lam_le := F.lam_le + spectral := F.spectral + +/-! ### The chosen namings -/ + +/-- Two darts of the base, as one dart of the level. -/ +def dartName : Fin F.deg × Fin F.deg ≃ Fin (F.deg ^ 2) := + finProdFinEquiv.trans (finCongr (by ring)) + +/-- The base's vertices name the pairs of darts of a level. -/ +def baseName : Fin (F.deg ^ 4) ≃ (Fin 2 → Fin (F.deg ^ 2)) := + (finCongr (show F.deg ^ 4 = F.deg ^ 2 * F.deg ^ 2 by ring)).trans + (finProdFinEquiv.symm.trans (finTwoArrowEquiv _).symm) + +/-- A level-`k` vertex together with a base vertex, as a level-`(k+1)` vertex. -/ +def vertName (k : ℕ) : + Fin (F.size k) × (Fin 2 → Fin (F.deg ^ 2)) ≃ Fin (F.size (k + 1)) := + ((Equiv.refl _).prodCongr F.baseName.symm).trans + (finProdFinEquiv.trans (finCongr (by simp only [size]; ring))) + +/-- The two darts of the squared base, as one. -/ +def zeroDartName : (Fin 2 → Fin F.deg) ≃ Fin (F.deg ^ 2) := + (finTwoArrowEquiv _).trans F.dartName + +/-- The squared base's vertices, numbered as level zero's. -/ +def zeroVertName : Fin (F.deg ^ 4) ≃ Fin (F.size 0) := + finCongr (by simp [size]) + +/-! ### The namings, in numbers -/ + +@[simp] theorem val_dartName (a b : Fin F.deg) : + (F.dartName (a, b)).val = b.val + F.deg * a.val := rfl + +@[simp] theorem val_dartName_symm_fst (i : Fin (F.deg ^ 2)) : + (F.dartName.symm i).1.val = i.val / F.deg := rfl + +@[simp] theorem val_dartName_symm_snd (i : Fin (F.deg ^ 2)) : + (F.dartName.symm i).2.val = i.val % F.deg := rfl + +@[simp] theorem val_baseName_zero (x : Fin (F.deg ^ 4)) : + (F.baseName x 0).val = x.val / F.deg ^ 2 := rfl + +@[simp] theorem val_baseName_one (x : Fin (F.deg ^ 4)) : + (F.baseName x 1).val = x.val % F.deg ^ 2 := rfl + +@[simp] theorem val_baseName_symm (s : Fin 2 → Fin (F.deg ^ 2)) : + (F.baseName.symm s).val = (s 1).val + F.deg ^ 2 * (s 0).val := rfl + +@[simp] theorem val_vertName (k : ℕ) (u : Fin (F.size k)) (s : Fin 2 → Fin (F.deg ^ 2)) : + (F.vertName k (u, s)).val = (F.baseName.symm s).val + F.deg ^ 4 * u.val := rfl + +@[simp] theorem val_vertName_symm_fst (k : ℕ) (v : Fin (F.size (k + 1))) : + ((F.vertName k).symm v).1.val = v.val / F.deg ^ 4 := rfl + +@[simp] theorem val_vertName_symm_snd_zero (k : ℕ) (v : Fin (F.size (k + 1))) : + (((F.vertName k).symm v).2 0).val = v.val % F.deg ^ 4 / F.deg ^ 2 := rfl + +@[simp] theorem val_vertName_symm_snd_one (k : ℕ) (v : Fin (F.size (k + 1))) : + (((F.vertName k).symm v).2 1).val = v.val % F.deg ^ 4 % F.deg ^ 2 := rfl + +theorem val_baseName_symm_vertName_symm (k : ℕ) (v : Fin (F.size (k + 1))) : + (F.baseName.symm ((F.vertName k).symm v).2).val = v.val % F.deg ^ 4 := by + have hlt : v.val % F.deg ^ 4 < F.deg ^ 4 := Nat.mod_lt _ (pow_pos F.deg_pos 4) + have hy : ((F.vertName k).symm v).2 = F.baseName ⟨v.val % F.deg ^ 4, hlt⟩ := rfl + rw [hy, Equiv.symm_apply_apply] + +@[simp] theorem val_zeroVertName (x : Fin (F.deg ^ 4)) : (F.zeroVertName x).val = x.val := rfl + +@[simp] theorem val_zeroVertName_symm (v : Fin (F.size 0)) : + (F.zeroVertName.symm v).val = v.val := rfl + +@[simp] theorem val_zeroDartName (s : Fin 2 → Fin F.deg) : + (F.zeroDartName s).val = (s 1).val + F.deg * (s 0).val := rfl + +@[simp] theorem val_zeroDartName_symm_zero (i : Fin (F.deg ^ 2)) : + (F.zeroDartName.symm i 0).val = i.val / F.deg := rfl + +@[simp] theorem val_zeroDartName_symm_one (i : Fin (F.deg ^ 2)) : + (F.zeroDartName.symm i 1).val = i.val % F.deg := rfl + +/-! ### The tower -/ + +/-- **The rotation map at each level**, carrying its involutivity so that the +next level's types are numbered on the nose. -/ +noncomputable def data (F : FinBase) : (k : ℕ) → + { f : Fin (F.size k) × Fin (F.deg ^ 2) → Fin (F.size k) × Fin (F.deg ^ 2) // + Function.Involutive f } + | 0 => + let W := ((F.graph.power 2).relabelV F.zeroVertName).relabel F.zeroDartName + ⟨W.rot, W.rot_involutive⟩ + | k + 1 => + let G := RegGraph.ofRot (F.deg ^ 2) F.sq_pos (F.size k) (data F k).1 (data F k).2 + let W := ((RegGraph.zigzag (G.power 2) F.graph F.baseName).relabelV + (F.vertName k)).relabel F.dartName + ⟨W.rot, W.rot_involutive⟩ + +/-- The level-`k` graph. -/ +noncomputable def graphAt (k : ℕ) : RegGraph := + RegGraph.ofRot (F.deg ^ 2) F.sq_pos (F.size k) (F.data k).1 (F.data k).2 + +@[simp] theorem order_graphAt (k : ℕ) : (F.graphAt k).order = F.size k := Fintype.card_fin _ + +@[simp] theorem deg_graphAt (k : ℕ) : (F.graphAt k).deg = F.deg ^ 2 := Fintype.card_fin _ + +theorem graphAt_zero : + F.graphAt 0 = ((F.graph.power 2).relabelV F.zeroVertName).relabel F.zeroDartName := by + simp only [graphAt, data] + rfl + +/-- **One level is the zig-zag of the previous with the base**, renumbered. -/ +theorem graphAt_succ (k : ℕ) : + F.graphAt (k + 1) = + ((RegGraph.zigzag ((F.graphAt k).power 2) F.graph F.baseName).relabelV + (F.vertName k)).relabel F.dartName := by + simp only [graphAt, data] + rfl + +/-- **Every level has spectral bound `2 / 5`.** -/ +theorem spectral_graphAt (F : FinBase) : ∀ k : ℕ, (F.graphAt k).SpectralBound (2 / 5) + | 0 => by + rw [graphAt_zero] + exact RegGraph.spectralBound_relabel _ _ + (RegGraph.spectralBound_relabelV _ _ F.toBase.towerZero.spec) + | k + 1 => by + rw [graphAt_succ] + exact RegGraph.spectralBound_relabel _ _ + (RegGraph.spectralBound_relabelV _ _ + (F.toBase.towerSuccOf ⟨F.graphAt k, by simp [toBase], spectral_graphAt F k⟩ + F.baseName).spec) + +/-- **One level of the recursion, in coordinates.** A level-`(k+1)` vertex is a +level-`k` vertex together with a base vertex, and a level-`(k+1)` dart is a pair +of base darts; in those coordinates a step is: turn in the base, walk two darts +of the level below, turn in the base again. -/ +theorem data_succ_apply (k : ℕ) (v : Fin (F.size (k + 1))) (i : Fin (F.deg ^ 2)) + {us : Fin (F.size k) × (Fin 2 → Fin (F.deg ^ 2))} (hus : us = (F.vertName k).symm v) + {ab : Fin F.deg × Fin F.deg} (hab : ab = F.dartName.symm i) + {p : Fin (F.deg ^ 4) × Fin F.deg} (hp : p = F.rot (F.baseName.symm us.2, ab.1)) + {q : Fin (F.size k) × (Fin 2 → Fin (F.deg ^ 2))} + (hq : q = ((F.graphAt k).power 2).rot (us.1, F.baseName p.1)) + {r : Fin (F.deg ^ 4) × Fin F.deg} (hr : r = F.rot (F.baseName.symm q.2, ab.2)) : + (F.data (k + 1)).1 (v, i) + = (F.vertName k (q.1, F.baseName r.1), F.dartName (r.2, p.2)) := by + subst hus hab hp hq hr + simp only [data, RegGraph.relabel, RegGraph.relabelV, RegGraph.zigzag, RegGraph.zigzagRot, + graphAt, graph, RegGraph.ofRot] + rfl + +@[simp] theorem rot_graphAt (k : ℕ) : (F.graphAt k).rot = (F.data k).1 := rfl + +/-- **The bottom of the recursion, in coordinates.** Level zero is the base +squared: walk two base darts, and return the labels in the opposite order. -/ +theorem data_zero_apply (v : Fin (F.size 0)) (i : Fin (F.deg ^ 2)) + {s : Fin 2 → Fin F.deg} (hs : s = F.zeroDartName.symm i) + {p₀ : Fin (F.deg ^ 4) × Fin F.deg} (h0 : p₀ = F.rot (F.zeroVertName.symm v, s 0)) + {p₁ : Fin (F.deg ^ 4) × Fin F.deg} (h1 : p₁ = F.rot (p₀.1, s 1)) : + (F.data 0).1 (v, i) = (F.zeroVertName p₁.1, F.zeroDartName ![p₁.2, p₀.2]) := by + subst hs h0 h1 + simp only [data, RegGraph.relabel, RegGraph.relabelV, RegGraph.rot_power_two, graph, + RegGraph.ofRot] + rfl + +/-- **The step in full**, with the two walks of the level below spelled out. -/ +theorem data_succ_apply' (k : ℕ) (v : Fin (F.size (k + 1))) (i : Fin (F.deg ^ 2)) + {us : Fin (F.size k) × (Fin 2 → Fin (F.deg ^ 2))} (hus : us = (F.vertName k).symm v) + {ab : Fin F.deg × Fin F.deg} (hab : ab = F.dartName.symm i) + {p : Fin (F.deg ^ 4) × Fin F.deg} (hp : p = F.rot (F.baseName.symm us.2, ab.1)) + {q₀ : Fin (F.size k) × Fin (F.deg ^ 2)} + (hq₀ : q₀ = (F.data k).1 (us.1, F.baseName p.1 0)) + {q₁ : Fin (F.size k) × Fin (F.deg ^ 2)} + (hq₁ : q₁ = (F.data k).1 (q₀.1, F.baseName p.1 1)) + {r : Fin (F.deg ^ 4) × Fin F.deg} + (hr : r = F.rot (F.baseName.symm ![q₁.2, q₀.2], ab.2)) : + (F.data (k + 1)).1 (v, i) + = (F.vertName k (q₁.1, F.baseName r.1), F.dartName (r.2, p.2)) := by + refine F.data_succ_apply k v i hus hab hp (q := (q₁.1, ![q₁.2, q₀.2])) ?_ hr + rw [RegGraph.rot_power_two, rot_graphAt, hq₁, hq₀] + rfl + +/-! ### The recursion, in numbers -/ + +/-- The base's rotation map, on raw numbers. -/ +noncomputable def baseVal (x a : ℕ) : ℕ × ℕ := + if h : x < F.deg ^ 4 ∧ a < F.deg then + ((F.rot (⟨x, h.1⟩, ⟨a, h.2⟩)).1.val, (F.rot (⟨x, h.1⟩, ⟨a, h.2⟩)).2.val) + else (0, 0) + +@[simp] theorem baseVal_apply (x : Fin (F.deg ^ 4)) (a : Fin F.deg) : + F.baseVal x.val a.val = ((F.rot (x, a)).1.val, (F.rot (x, a)).2.val) := by + rw [baseVal, dif_pos ⟨x.isLt, a.isLt⟩] + +/-- **The tower's rotation map, on raw numbers.** A level-`(k+1)` vertex `v` +splits as `v / deg^4` (the level below) and `v % deg^4` (the base); a dart `i` +splits as `i / deg` and `i % deg`. The step turns in the base, walks two darts +of the level below, and turns in the base again. -/ +noncomputable def rotVal (F : FinBase) : ℕ → ℕ × ℕ → ℕ × ℕ + | 0, (v, i) => + let p₀ := F.baseVal v (i / F.deg) + let p₁ := F.baseVal p₀.1 (i % F.deg) + (p₁.1, p₀.2 + F.deg * p₁.2) + | k + 1, (v, i) => + let p := F.baseVal (v % F.deg ^ 4) (i / F.deg) + let q₀ := rotVal F k (v / F.deg ^ 4, p.1 / F.deg ^ 2) + let q₁ := rotVal F k (q₀.1, p.1 % F.deg ^ 2) + let r := F.baseVal (q₀.2 + F.deg ^ 2 * q₁.2) (i % F.deg) + (r.1 + F.deg ^ 4 * q₁.1, p.2 + F.deg * r.2) + +/-- **The numbers compute the tower.** -/ +theorem rotVal_eq (k : ℕ) (v : Fin (F.size k)) (i : Fin (F.deg ^ 2)) : + F.rotVal k (v.val, i.val) + = (((F.data k).1 (v, i)).1.val, ((F.data k).1 (v, i)).2.val) := by + induction k generalizing i with + | zero => + have e0 : F.baseVal v.val (i.val / F.deg) + = ((F.rot (F.zeroVertName.symm v, F.zeroDartName.symm i 0)).1.val, + (F.rot (F.zeroVertName.symm v, F.zeroDartName.symm i 0)).2.val) := + F.baseVal_apply (F.zeroVertName.symm v) (F.zeroDartName.symm i 0) + have e1 : F.baseVal (F.rot (F.zeroVertName.symm v, F.zeroDartName.symm i 0)).1.val + (i.val % F.deg) + = ((F.rot ((F.rot (F.zeroVertName.symm v, F.zeroDartName.symm i 0)).1, + F.zeroDartName.symm i 1)).1.val, + (F.rot ((F.rot (F.zeroVertName.symm v, F.zeroDartName.symm i 0)).1, + F.zeroDartName.symm i 1)).2.val) := + F.baseVal_apply (F.rot (F.zeroVertName.symm v, F.zeroDartName.symm i 0)).1 + (F.zeroDartName.symm i 1) + rw [F.data_zero_apply v i rfl rfl rfl] + simp only [rotVal] + rw [e0, e1] + simp + | succ k ih => + have hp := F.baseVal_apply (F.baseName.symm ((F.vertName k).symm v).2) + (F.dartName.symm i).1 + rw [F.val_baseName_symm_vertName_symm k v, val_dartName_symm_fst] at hp + have hq0 := ih ((F.vertName k).symm v).1 + (F.baseName (F.rot (F.baseName.symm ((F.vertName k).symm v).2, + (F.dartName.symm i).1)).1 0) + have hq1 := ih ((F.data k).1 (((F.vertName k).symm v).1, + F.baseName (F.rot (F.baseName.symm ((F.vertName k).symm v).2, + (F.dartName.symm i).1)).1 0)).1 + (F.baseName (F.rot (F.baseName.symm ((F.vertName k).symm v).2, + (F.dartName.symm i).1)).1 1) + have hr := F.baseVal_apply (F.baseName.symm + ![((F.data k).1 (((F.data k).1 (((F.vertName k).symm v).1, + F.baseName (F.rot (F.baseName.symm ((F.vertName k).symm v).2, + (F.dartName.symm i).1)).1 0)).1, + F.baseName (F.rot (F.baseName.symm ((F.vertName k).symm v).2, + (F.dartName.symm i).1)).1 1)).2, + ((F.data k).1 (((F.vertName k).symm v).1, + F.baseName (F.rot (F.baseName.symm ((F.vertName k).symm v).2, + (F.dartName.symm i).1)).1 0)).2]) (F.dartName.symm i).2 + rw [val_baseName_symm, val_dartName_symm_snd] at hr + simp only [Matrix.cons_val_zero, Matrix.cons_val_one] at hr + rw [val_vertName_symm_fst, val_baseName_zero] at hq0 + rw [val_baseName_one] at hq1 + rw [F.data_succ_apply' k v i rfl rfl rfl rfl rfl rfl] + simp only [rotVal] + rw [hp] + dsimp only + rw [hq0] + dsimp only + rw [hq1] + dsimp only + rw [hr] + simp + +theorem baseVal_lt {x a : ℕ} (hx : x < F.deg ^ 4) (ha : a < F.deg) : + (F.baseVal x a).1 < F.deg ^ 4 ∧ (F.baseVal x a).2 < F.deg := by + rw [baseVal, dif_pos ⟨hx, ha⟩] + exact ⟨Fin.isLt _, Fin.isLt _⟩ + +theorem rotVal_lt (k : ℕ) {v i : ℕ} (hv : v < F.size k) (hi : i < F.deg ^ 2) : + (F.rotVal k (v, i)).1 < F.size k ∧ (F.rotVal k (v, i)).2 < F.deg ^ 2 := by + rw [show v = (⟨v, hv⟩ : Fin (F.size k)).val from rfl, + show i = (⟨i, hi⟩ : Fin (F.deg ^ 2)).val from rfl, F.rotVal_eq k] + exact ⟨Fin.isLt _, Fin.isLt _⟩ + +/-! ### Choosing a level -/ + +theorem one_lt_pow_four (hd : 1 < F.deg) : 1 < F.deg ^ 4 := Nat.one_lt_pow (by norm_num) hd + +theorem exists_size_ge (hd : 1 < F.deg) (n : ℕ) : ∃ k, n ≤ F.size k := by + refine ⟨n, ?_⟩ + calc n ≤ (F.deg ^ 4) ^ n := (Nat.lt_pow_self (F.one_lt_pow_four hd)).le + _ ≤ (F.deg ^ 4) ^ (n + 1) := + Nat.pow_le_pow_right (Nat.zero_lt_of_lt (F.one_lt_pow_four hd)) (by omega) + +theorem le_size_self (hd : 1 < F.deg) (n : ℕ) : n ≤ F.size n := by + calc n ≤ (F.deg ^ 4) ^ n := (Nat.lt_pow_self (F.one_lt_pow_four hd)).le + _ ≤ (F.deg ^ 4) ^ (n + 1) := + Nat.pow_le_pow_right (Nat.zero_lt_of_lt (F.one_lt_pow_four hd)) (by omega) + +/-- The first level large enough for a requested size. -/ +noncomputable def level (F : FinBase) (hd : 1 < F.deg) (n : ℕ) : ℕ := + Nat.find (F.exists_size_ge hd n) + +theorem le_size_level (hd : 1 < F.deg) (n : ℕ) : n ≤ F.size (F.level hd n) := + Nat.find_spec (F.exists_size_ge hd n) + +/-- **The level a size needs is at most that size.** -/ +theorem level_le (hd : 1 < F.deg) (n : ℕ) : F.level hd n ≤ n := + Nat.find_le (F.le_size_self hd n) + +theorem size_level_le (hd : 1 < F.deg) (n : ℕ) (hn : 1 ≤ n) : + F.size (F.level hd n) ≤ F.deg ^ 4 * n := by + classical + rcases Nat.eq_zero_or_pos (F.level hd n) with h0 | hpos + · rw [level] at h0 ⊢ + rw [h0, size] + calc (F.deg ^ 4) ^ (0 + 1) = F.deg ^ 4 := by ring + _ ≤ F.deg ^ 4 * n := Nat.le_mul_of_pos_right _ hn + · obtain ⟨m, hm⟩ : ∃ m, F.level hd n = m + 1 := ⟨F.level hd n - 1, by omega⟩ + have hfind : Nat.find (F.exists_size_ge hd n) = m + 1 := hm + have hlt : ¬ n ≤ F.size m := + Nat.find_min (F.exists_size_ge hd n) (m := m) (by rw [hfind]; omega) + have hprev : F.size m < n := by omega + rw [hm, size] + rw [size] at hprev + calc (F.deg ^ 4) ^ (m + 1 + 1) = F.deg ^ 4 * (F.deg ^ 4) ^ (m + 1) := by ring + _ ≤ F.deg ^ 4 * n := Nat.mul_le_mul_left _ hprev.le + +end FinBase + +/-! ### Numbering a base -/ + +namespace ZigZagBase + +variable (B : ZigZagBase) + +theorem card_base_darts : Fintype.card B.base.D = B.base.deg := rfl + +/-- **A zig-zag base, numbered.** -/ +noncomputable def toFin : FinBase where + deg := B.base.deg + deg_pos := B.base.deg_pos + rot := (B.base.toFinFormOf (B.base.deg ^ 4) B.base.deg B.card_eq B.card_base_darts).rot + rot_involutive := + (B.base.toFinFormOf (B.base.deg ^ 4) B.base.deg B.card_eq B.card_base_darts).rot_involutive + lam := B.lam + lam_nonneg := B.lam_nonneg + lam_le := B.lam_le + spectral := + RegGraph.spectralBound_toFinFormOf B.base (B.base.deg ^ 4) B.base.deg B.card_eq + B.card_base_darts B.spectral + +end ZigZagBase + +/-- **And one whose degree is above one**, which is what folding a tower onto a +requested size needs. -/ +theorem exists_finBase : ∃ F : FinBase, 1 < F.deg := + ⟨randExpander.toZigZagBase.toFin, + randExpander.one_lt_deg_toZigZagBase (by + show 1 < 120 + norm_num)⟩ + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/TowerTable.lean b/Complexitylib/Classes/PCP/Internal/TowerTable.lean new file mode 100644 index 00000000..c751853c --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/TowerTable.lean @@ -0,0 +1,326 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.PCP.Internal.TowerFin +public import Complexitylib.Classes.PCP.Internal.Materialize +public import Complexitylib.Classes.P.FinsetDomain +public import Complexitylib.Classes.PCP.Internal.FiniteKey + +/-! +# The tower's rotation table + +`TowerFin` gives the tower's rotation map as arithmetic on numbers; this module +runs that arithmetic. A level is held as a table — one record for each vertex +and dart, holding the vertex reached and the label to come back by, both in +unary — and one level is computed from the one below by writing a new table +whose every record needs two lookups in the old one. + +The base graph's own rotation map is a table on a bounded key, so it is +polynomial time however it was chosen (`FiniteKey`). + +## Main definitions + +- `Complexity.FinBase.tableList`, `Complexity.FinBase.table` — a level's table +- `Complexity.FinBase.baseRec` — the base's rotation map, as a record +- `Complexity.FinBase.stepRec` — one record of the next level + +## Main results + +- `Complexity.FinBase.baseRec_mem_FP`, `Complexity.FinBase.stepRec_mem_FP` +- `Complexity.FinBase.stepRec_eq` — the rule computes the level above +- `Complexity.FinBase.tableStep_eq`, `Complexity.FinBase.tableStep_mem_FP` — and + writing out every record climbs one level +- `Complexity.FinBase.table_mem_FP` — so the table of any level is polynomial + time, given room for it +-/ + +@[expose] public section + +namespace Complexity + +theorem mul_add_div_of_lt {a b c : ℕ} (hc : 0 < c) (h : b < c) : (a * c + b) / c = a := by + rw [show a * c + b = b + c * a by ring, Nat.add_mul_div_left _ _ hc, + Nat.div_eq_of_lt h, Nat.zero_add] + +theorem mul_add_mod_of_lt {a b c : ℕ} (h : b < c) : (a * c + b) % c = b := by + rw [show a * c + b = b + c * a by ring, Nat.add_mul_mod_self_left, Nat.mod_eq_of_lt h] + +namespace FinBase + +variable (F : FinBase) + +/-! ### The table -/ + +/-- The records of the level-`k` table: for each vertex and dart, the vertex +reached and the label pointing back, both in unary. -/ +noncomputable def tableList (k : ℕ) : List (List Bool × List Bool) := + (List.range (F.size k * F.deg ^ 2)).map fun j => + (List.replicate (F.rotVal k (j / F.deg ^ 2, j % F.deg ^ 2)).1 true, + List.replicate (F.rotVal k (j / F.deg ^ 2, j % F.deg ^ 2)).2 true) + +@[simp] theorem length_tableList (k : ℕ) : + (F.tableList k).length = F.size k * F.deg ^ 2 := by + rw [tableList, List.length_map, List.length_range] + +/-- The level-`k` table. -/ +noncomputable def table (k : ℕ) : List Bool := DataEncode.bitstringEncode (F.tableList k) + +theorem tableFst_table {k j : ℕ} (hj : j < F.size k * F.deg ^ 2) : + tableFst (F.table k) j + = List.replicate (F.rotVal k (j / F.deg ^ 2, j % F.deg ^ 2)).1 true := by + rw [table] + refine tableFst_eq (l := F.tableList k) + (c := (F.rotVal k (j / F.deg ^ 2, j % F.deg ^ 2)).2) + (by rw [length_tableList]; exact hj) ?_ + simp only [tableList, List.getElem_map, List.getElem_range] + +theorem tableSnd_table {k j : ℕ} (hj : j < F.size k * F.deg ^ 2) : + tableSnd (F.table k) j + = List.replicate (F.rotVal k (j / F.deg ^ 2, j % F.deg ^ 2)).2 true := by + rw [table] + refine tableSnd_eq (l := F.tableList k) + (w := (F.rotVal k (j / F.deg ^ 2, j % F.deg ^ 2)).1) + (by rw [length_tableList]; exact hj) ?_ + simp only [tableList, List.getElem_map, List.getElem_range] + +/-! ### The base graph's own table -/ + +/-- The record the base's rotation map gives, before the key is bounded. -/ +noncomputable def baseRaw (z : List Bool) : List Bool := + encPair + (List.replicate + (F.baseVal (pairFst z).length (pairSnd z).length).1 true) + (List.replicate + (F.baseVal (pairFst z).length (pairSnd z).length).2 true) + +/-- How long an argument to the base's table can be. -/ +def baseKeyBound : ℕ := 2 * F.deg ^ 4 + 2 + F.deg + +/-- **The base's rotation map, as a record.** -/ +noncomputable def baseRec (z : List Bool) : List Bool := + if z ∈ keySet F.baseKeyBound (fun _ => True) then F.baseRaw z else [] + +theorem baseRec_mem_FP : F.baseRec ∈ FP := + ite_mem_finset_mem_FP F.baseRaw (keySet F.baseKeyBound (fun _ => True)) + +theorem baseRec_eq {x a : ℕ} (hx : x < F.deg ^ 4) (ha : a < F.deg) : + F.baseRec (pair (List.replicate x true) (List.replicate a true)) + = encPair (List.replicate (F.baseVal x a).1 true) + (List.replicate (F.baseVal x a).2 true) := by + have hlen : (pair (List.replicate x true) (List.replicate a true)).length + ≤ F.baseKeyBound := by + rw [pair_length, List.length_replicate, List.length_replicate, baseKeyBound] + omega + rw [baseRec, if_pos (mem_keySet.mpr ⟨hlen, trivial⟩), baseRaw, + pairFst_pair, pairSnd_pair, List.length_replicate, + List.length_replicate] + +/-! ### One record of the next level -/ + +/-- One record of the level above, from the table below. The argument is +`pair table (unary index)`: the index splits into a vertex and a dart, the +vertex into a vertex of the level below and a base vertex, and the dart into two +base darts. -/ +noncomputable def stepRec (z : List Bool) : List Bool := + let T := pairFst z + let J := pairSnd z + let V := divC (F.deg ^ 2) J + let I := modC (F.deg ^ 2) J + let U := divC (F.deg ^ 4) V + let X := modC (F.deg ^ 4) V + let A := divC F.deg I + let B := modC F.deg I + let P := F.baseRec (pair X A) + let P1 := unaryOf (fstEnc P) + let P2 := unaryOf (sndEnc P) + let Q0 := posAt T (mulC (F.deg ^ 2) U ++ divC (F.deg ^ 2) P1).length + let W0 := unaryOf (fstEnc Q0) + let C0 := unaryOf (sndEnc Q0) + let Q1 := posAt T (mulC (F.deg ^ 2) W0 ++ modC (F.deg ^ 2) P1).length + let W1 := unaryOf (fstEnc Q1) + let C1 := unaryOf (sndEnc Q1) + let R := F.baseRec (pair (marks (C0 ++ mulC (F.deg ^ 2) C1)) B) + let R1 := unaryOf (fstEnc R) + let R2 := unaryOf (sndEnc R) + encPair (marks (R1 ++ mulC (F.deg ^ 4) W1)) (marks (P2 ++ mulC F.deg R2)) + +theorem stepRec_mem_FP : F.stepRec ∈ FP := by + have hT := Cobham.fstBlock_mem_FP + have hJ := Cobham.sndBlock_mem_FP + have hV := divC_mem_FP hJ (F.deg ^ 2) + have hI := modC_mem_FP hJ (F.deg ^ 2) + have hU := divC_mem_FP hV (F.deg ^ 4) + have hX := modC_mem_FP hV (F.deg ^ 4) + have hA := divC_mem_FP hI F.deg + have hB := modC_mem_FP hI F.deg + have hP := mem_FP_comp (Cobham.pairFn_mem_FP hX hA) F.baseRec_mem_FP + have hP1 := unaryOf_mem_FP (fstEnc_mem_FP hP) + have hP2 := unaryOf_mem_FP (sndEnc_mem_FP hP) + have hIdx0 := Cobham.appendFn_mem_FP (mulC_mem_FP hU (F.deg ^ 2)) + (divC_mem_FP hP1 (F.deg ^ 2)) + have hQ0 := posAt_mem_FP hIdx0 hT + have hW0 := unaryOf_mem_FP (fstEnc_mem_FP hQ0) + have hC0 := unaryOf_mem_FP (sndEnc_mem_FP hQ0) + have hIdx1 := Cobham.appendFn_mem_FP (mulC_mem_FP hW0 (F.deg ^ 2)) + (modC_mem_FP hP1 (F.deg ^ 2)) + have hQ1 := posAt_mem_FP hIdx1 hT + have hW1 := unaryOf_mem_FP (fstEnc_mem_FP hQ1) + have hC1 := unaryOf_mem_FP (sndEnc_mem_FP hQ1) + have hRarg := Cobham.pairFn_mem_FP + (marks_mem_FP (Cobham.appendFn_mem_FP hC0 (mulC_mem_FP hC1 (F.deg ^ 2)))) hB + have hR := mem_FP_comp hRarg F.baseRec_mem_FP + have hR1 := unaryOf_mem_FP (fstEnc_mem_FP hR) + have hR2 := unaryOf_mem_FP (sndEnc_mem_FP hR) + have hOut := encPair_mem_FP + (marks_mem_FP (Cobham.appendFn_mem_FP hR1 (mulC_mem_FP hW1 (F.deg ^ 4)))) + (marks_mem_FP (Cobham.appendFn_mem_FP hP2 (mulC_mem_FP hR2 F.deg))) + exact mem_FP_of_eq hOut fun _ => rfl + +/-- **The record rule computes the level above.** -/ +theorem stepRec_eq {k j : ℕ} (hj : j < F.size (k + 1) * F.deg ^ 2) : + F.stepRec (pair (F.table k) (List.replicate j true)) + = encPair + (List.replicate (F.rotVal (k + 1) (j / F.deg ^ 2, j % F.deg ^ 2)).1 true) + (List.replicate (F.rotVal (k + 1) (j / F.deg ^ 2, j % F.deg ^ 2)).2 true) := by + have hd1 : 0 < F.deg := F.deg_pos + have hd2 : 0 < F.deg ^ 2 := F.sq_pos + have hd4 : 0 < F.deg ^ 4 := pow_pos F.deg_pos 4 + have hsq : F.deg ^ 4 = F.deg ^ 2 * F.deg ^ 2 := by ring + -- the vertex and the dart + have hvlt : j / F.deg ^ 2 < F.size (k + 1) := (Nat.div_lt_iff_lt_mul hd2).mpr hj + have hxlt : j / F.deg ^ 2 % F.deg ^ 4 < F.deg ^ 4 := Nat.mod_lt _ hd4 + have halt : j % F.deg ^ 2 / F.deg < F.deg := by + refine (Nat.div_lt_iff_lt_mul hd1).mpr ?_ + have := Nat.mod_lt j hd2 + nlinarith [this] + have hult : j / F.deg ^ 2 / F.deg ^ 4 < F.size k := by + refine (Nat.div_lt_iff_lt_mul hd4).mpr ?_ + rw [← F.size_succ k] + exact hvlt + -- the base turn + obtain ⟨p1, p2, hp⟩ : ∃ p1 p2, + F.baseVal (j / F.deg ^ 2 % F.deg ^ 4) (j % F.deg ^ 2 / F.deg) = (p1, p2) := ⟨_, _, rfl⟩ + have hp1 : p1 < F.deg ^ 4 := by + have := (F.baseVal_lt hxlt halt).1 + rwa [hp] at this + rw [stepRec] + simp only [pairFst_pair, pairSnd_pair, divC_eq hd2, modC_eq hd2, + divC_eq hd4, modC_eq hd4, divC_eq hd1, modC_eq hd1, List.length_replicate, + F.baseRec_eq hxlt halt, hp, unaryOf_fstEnc_encPair, unaryOf_sndEnc_encPair, + length_mulC_append, ← tableFst_def, ← tableSnd_def] + -- the first lookup + have hs0 : p1 / F.deg ^ 2 < F.deg ^ 2 := + (Nat.div_lt_iff_lt_mul hd2).mpr (by rw [← hsq]; exact hp1) + have hs1 : p1 % F.deg ^ 2 < F.deg ^ 2 := Nat.mod_lt _ hd2 + have hidx0 : j / F.deg ^ 2 / F.deg ^ 4 * F.deg ^ 2 + p1 / F.deg ^ 2 < F.size k * F.deg ^ 2 := by + have h2 : j / F.deg ^ 2 / F.deg ^ 4 + 1 ≤ F.size k := hult + nlinarith + obtain ⟨w0, c0, hq0⟩ : ∃ w0 c0, + F.rotVal k (j / F.deg ^ 2 / F.deg ^ 4, p1 / F.deg ^ 2) = (w0, c0) := ⟨_, _, rfl⟩ + have hw0 : w0 < F.size k := by + have := (F.rotVal_lt k hult hs0).1 + rwa [hq0] at this + have hc0 : c0 < F.deg ^ 2 := by + have := (F.rotVal_lt k hult hs0).2 + rwa [hq0] at this + rw [F.tableFst_table hidx0, F.tableSnd_table hidx0, + mul_add_div_of_lt hd2 hs0, mul_add_mod_of_lt hs0, hq0] + simp only [length_mulC_append] + -- the second lookup + have hidx1 : w0 * F.deg ^ 2 + p1 % F.deg ^ 2 < F.size k * F.deg ^ 2 := by + have h2 : w0 + 1 ≤ F.size k := hw0 + nlinarith + obtain ⟨w1, c1, hq1⟩ : ∃ w1 c1, + F.rotVal k (w0, p1 % F.deg ^ 2) = (w1, c1) := ⟨_, _, rfl⟩ + have hc1 : c1 < F.deg ^ 2 := by + have := (F.rotVal_lt k hw0 hs1).2 + rwa [hq1] at this + rw [F.tableFst_table hidx1, F.tableSnd_table hidx1, + mul_add_div_of_lt hd2 hs1, mul_add_mod_of_lt hs1, hq1] + simp only [marks_append_mulC] + -- the base turn back + have hblt : j % F.deg ^ 2 % F.deg < F.deg := Nat.mod_lt _ hd1 + have hrlt : c0 + F.deg ^ 2 * c1 < F.deg ^ 4 := by + rw [hsq] + nlinarith + rw [show c0 + c1 * F.deg ^ 2 = c0 + F.deg ^ 2 * c1 from by ring] + obtain ⟨r1, r2, hr⟩ : ∃ r1 r2, + F.baseVal (c0 + F.deg ^ 2 * c1) (j % F.deg ^ 2 % F.deg) = (r1, r2) := ⟨_, _, rfl⟩ + rw [F.baseRec_eq hrlt hblt, hr] + simp only [unaryOf_fstEnc_encPair, unaryOf_sndEnc_encPair, marks_append_mulC] + simp only [rotVal, hp, hq0, hq1, hr] + rw [show r1 + w1 * F.deg ^ 4 = r1 + F.deg ^ 4 * w1 from by ring, + show p2 + r2 * F.deg = p2 + F.deg * r2 from by ring] + +/-! ### One level of the table -/ + +/-- **One level of the table**: write out every record of the level above. -/ +noncomputable def tableStep (T : List Bool) : List Bool := + listEncFn F.stepRec (pair (marks (mulC (F.deg ^ 4) (posCount T))) T) + +theorem tableStep_mem_FP : F.tableStep ∈ FP := by + have hcount := marks_mem_FP (mulC_mem_FP (posCount_mem_FP id_mem_FP) (F.deg ^ 4)) + have harg := Cobham.pairFn_mem_FP hcount id_mem_FP + exact mem_FP_of_eq (mem_FP_comp harg (materialize_mem_FP F.stepRec_mem_FP)) fun _ => rfl + +theorem tableStep_eq (k : ℕ) : F.tableStep (F.table k) = F.table (k + 1) := by + have hcount : marks (mulC (F.deg ^ 4) (posCount (F.table k))) + = List.replicate (F.tableList (k + 1)).length true := by + rw [table, posCount_eq, marks_eq, length_mulC, List.length_replicate, length_tableList, + length_tableList, F.size_succ k] + ring_nf + rw [tableStep, hcount, table] + refine materialize_eq (F.tableList (k + 1)) (F.table k) fun i hi => ?_ + rw [length_tableList] at hi + rw [F.stepRec_eq hi] + simp only [tableList, List.getElem_map, List.getElem_range] + rw [encPair_eq] + +/-- How long a level's table is: one record a vertex and dart, and a record +holds two numbers below the level's size. -/ +theorem length_table_le (l : ℕ) : + (F.table l).length + ≤ 2 + F.size l * F.deg ^ 2 * (4 * F.size l + 4 * F.deg ^ 2 + 6) := by + rw [table, length_bitstringEncode_list] + have hbound : ∀ x ∈ (F.tableList l).map + (fun a => (DataEncode.bitstringEncode a).length), + x ≤ 4 * F.size l + 4 * F.deg ^ 2 + 6 := by + intro x hx + simp only [List.mem_map] at hx + obtain ⟨a, ha, rfl⟩ := hx + simp only [tableList, List.mem_map, List.mem_range] at ha + obtain ⟨j, hj, rfl⟩ := ha + have hv : j / F.deg ^ 2 < F.size l := (Nat.div_lt_iff_lt_mul F.sq_pos).mpr hj + have hi : j % F.deg ^ 2 < F.deg ^ 2 := Nat.mod_lt _ F.sq_pos + have hlt := F.rotVal_lt l hv hi + rw [← encPair_eq, length_encPair] + omega + have hsum := List.sum_le_card_nsmul _ _ hbound + rw [List.length_map, length_tableList] at hsum + simp only [smul_eq_mul] at hsum + omega + +/-! ### Climbing to a level -/ + +theorem tableStep_iterate : ∀ l : ℕ, F.tableStep^[l] (F.table 0) = F.table l + | 0 => rfl + | l + 1 => by + rw [Function.iterate_succ_apply', tableStep_iterate l, tableStep_eq] + +/-- **The table of a requested level is polynomial time**, as soon as there is +room to write it down. -/ +theorem table_mem_FP {ruler width : List Bool → List Bool} (hr : ruler ∈ FP) (hw : width ∈ FP) + (hbound : ∀ z, ∀ l ≤ (ruler z).length, (F.table l).length ≤ (width z).length) : + (fun z => F.table (ruler z).length) ∈ FP := by + have hiter := Cobham.iterate_mem_FP F.tableStep_mem_FP (constFn_mem_FP (F.table 0)) hr hw + (fun z l hl => by rw [F.tableStep_iterate l]; exact hbound z l hl) + exact mem_FP_of_eq hiter fun z => by rw [F.tableStep_iterate] + +end FinBase + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/UnaryDivMod.lean b/Complexitylib/Classes/PCP/Internal/UnaryDivMod.lean new file mode 100644 index 00000000..634944d9 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/UnaryDivMod.lean @@ -0,0 +1,270 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.HeadOps +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.PCP.Internal.CoinEnum +public import Complexitylib.Classes.P.Cobham.Internal.PolyRuler + +/-! +# Division with remainder, in unary + +Every index decomposition in an algorithmic constraint graph is a division: +which edge of the original graph, which step of the walk, which copy of the +gadget. This module divides one unary number by another in polynomial time, by +counting up and rolling the remainder over. + +## Main definitions + +- `Complexity.dmStep` — one tick of the count + +## Main results + +- `Complexity.dmStep_iterate` — the count divides +- `Complexity.halfFn_mem_FP`, `Complexity.halfFn_eq` — halving a length +-/ + +@[expose] public section + +namespace Complexity + +/-- One tick: extend the remainder, and roll it over into the quotient when it +reaches the divisor. The state is `pair (pair quotient remainder) divisor`. -/ +def dmStep (st : List Bool) : List Bool := + pair (pair + (Cobham.selectHead + (Cobham.lenEqFlag (true :: pairSnd (pairFst st)) (pairSnd st)) + (true :: pairFst (pairFst st)) + (pairFst (pairFst st))) + (Cobham.selectHead + (Cobham.lenEqFlag (true :: pairSnd (pairFst st)) (pairSnd st)) + [] (true :: pairSnd (pairFst st)))) + (pairSnd st) + +theorem dmStep_mem_FP : dmStep ∈ FP := by + have hq : (fun st : List Bool => pairFst (pairFst st)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + have hr : (fun st : List Bool => pairSnd (pairFst st)) ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + have hb : (fun st : List Bool => pairSnd st) ∈ FP := Cobham.sndBlock_mem_FP + have hr' : (fun st : List Bool => true :: pairSnd (pairFst st)) ∈ FP := + mem_FP_comp hr (Cobham.cons_mem_FP true) + have hflag : (fun st : List Bool => + Cobham.lenEqFlag (true :: pairSnd (pairFst st)) + (pairSnd st)) ∈ FP := by + exact andBitFn_mem_FP (lenLeFlagFn_mem_FP hr' hb) (lenLeFlagFn_mem_FP hb hr') + exact Cobham.pairFn_mem_FP + (Cobham.pairFn_mem_FP + (Cobham.selectHeadFn_mem_FP hflag (mem_FP_comp hq (Cobham.cons_mem_FP true)) hq) + (Cobham.selectHeadFn_mem_FP hflag (constFn_mem_FP []) hr')) hb + +/-- **The count divides.** After `a` ticks the state holds `a / b` and `a % b`. -/ +theorem dmStep_iterate {B : List Bool} (hb : 0 < B.length) : + ∀ a : ℕ, dmStep^[a] (pair (pair [] []) B) + = pair (pair (List.replicate (a / B.length) true) + (List.replicate (a % B.length) true)) B := by + intro a + induction a with + | zero => simp + | succ a ih => + rw [Function.iterate_succ_apply', ih, dmStep, pairSnd_pair, + pairFst_pair, pairFst_pair, pairSnd_pair] + set b := B.length with hbdef + have hlen : (true :: List.replicate (a % b) true).length = a % b + 1 := by simp + have hmod : a % b < b := Nat.mod_lt _ hb + by_cases hcase : a % b + 1 = b + · have hflag : Cobham.lenEqFlag (true :: List.replicate (a % b) true) B = [true] := by + rw [Cobham.lenEqFlag_eq_true_iff, hlen] + exact hcase + rw [hflag, selectHead_cons_true, selectHead_cons_true] + have hdm := Nat.div_add_mod a b + have h1 : a + 1 = b * (a / b + 1) := by + rw [Nat.mul_add, Nat.mul_one] + omega + have hq : (a + 1) / b = a / b + 1 := by + rw [h1, Nat.mul_div_cancel_left _ hb] + have hr : (a + 1) % b = 0 := by + rw [h1, Nat.mul_mod_right] + rw [hq, hr, List.replicate_succ] + simp + · have hflag : Cobham.lenEqFlag (true :: List.replicate (a % b) true) B = [false] := by + rcases Cobham.lenEqFlag_flag (true :: List.replicate (a % b) true) B with h | h + · rw [Cobham.lenEqFlag_eq_true_iff, hlen] at h + exact absurd h hcase + · exact h + rw [hflag, selectHead_cons_false, selectHead_cons_false] + have hdm := Nat.div_add_mod a b + have hlt : a % b + 1 < b := by omega + have h1 : a + 1 = b * (a / b) + (a % b + 1) := by omega + have hq : (a + 1) / b = a / b := by + rw [h1, Nat.mul_add_div hb, Nat.div_eq_of_lt hlt] + omega + have hr : (a + 1) % b = a % b + 1 := by + rw [h1, Nat.mul_add_mod, Nat.mod_eq_of_lt hlt] + rw [hq, hr, List.replicate_succ] + +/-! ### Halving -/ + +theorem length_selectHead_le (s x y : List Bool) : + (Cobham.selectHead s x y).length ≤ max x.length y.length := by + rw [Cobham.selectHead] + split + · exact le_max_left _ _ + · split + · exact le_max_right _ _ + · simp + +theorem dmStep_one (q r b : List Bool) : + ∃ q' r', dmStep (pair (pair q r) b) = pair (pair q' r') b + ∧ q'.length ≤ q.length + 1 ∧ r'.length ≤ r.length + 1 := by + have hq : pairFst (pairFst (pair (pair q r) b)) = q := by + rw [pairFst_pair, pairFst_pair] + have hr : pairSnd (pairFst (pair (pair q r) b)) = r := by + rw [pairFst_pair, pairSnd_pair] + have hb : pairSnd (pair (pair q r) b) = b := pairSnd_pair _ _ + rw [dmStep, hq, hr, hb] + refine ⟨_, _, rfl, ?_, ?_⟩ + · refine le_trans (length_selectHead_le _ _ _) ?_ + simp + · refine le_trans (length_selectHead_le _ _ _) ?_ + simp + +theorem dmStep_shape : ∀ (k : ℕ) (q r b : List Bool), + ∃ q' r', dmStep^[k] (pair (pair q r) b) = pair (pair q' r') b + ∧ q'.length ≤ q.length + k ∧ r'.length ≤ r.length + k := by + intro k + induction k with + | zero => intro q r b; exact ⟨q, r, rfl, by omega, by omega⟩ + | succ k ih => + intro q r b + rw [Function.iterate_succ_apply] + obtain ⟨q₁, r₁, h1, hq1, hr1⟩ := dmStep_one q r b + rw [h1] + obtain ⟨q', r', h2, hq2, hr2⟩ := ih q₁ r₁ b + exact ⟨q', r', h2, by omega, by omega⟩ + +/-- The counting run: divide a length by a fixed divisor. -/ +noncomputable def dmRun (b s : List Bool) : List Bool := + dmStep^[s.length] (pair (pair [] []) b) + +/-- The quotient of a length by a fixed divisor, in unary. -/ +noncomputable def divFn (b s : List Bool) : List Bool := + pairFst (pairFst (dmRun b s)) + +/-- The remainder of a length by a fixed divisor, in unary. -/ +noncomputable def modFn (b s : List Bool) : List Bool := + pairSnd (pairFst (dmRun b s)) + +theorem dmRun_mem_FP (b : List Bool) : dmRun b ∈ FP := by + have hinit : (fun _ : List Bool => pair (pair [] []) b) ∈ FP := constFn_mem_FP _ + have hwidth : (fun z : List Bool => polyRuler (Polynomial.C 6 * Polynomial.X + + Polynomial.C (b.length + 6)) (id z)) ∈ FP := polyRulerFn_mem_FP _ id_mem_FP + have hbound : ∀ z : List Bool, ∀ k ≤ z.length, + (dmStep^[k] (pair (pair [] []) b)).length + ≤ (polyRuler (Polynomial.C 6 * Polynomial.X + + Polynomial.C (b.length + 6)) (id z)).length := by + intro z k hk + obtain ⟨q', r', h1, hq, hr⟩ := dmStep_shape k [] [] b + rw [h1, pair_length, pair_length, polyRuler_length] + simp only [Polynomial.eval_add, Polynomial.eval_mul, Polynomial.eval_C, + Polynomial.eval_X, id, List.length_nil, Nat.zero_add] at * + omega + have hiter := Cobham.iterate_mem_FP dmStep_mem_FP hinit id_mem_FP hwidth hbound + simpa using hiter + +theorem divFn_mem_FP (b : List Bool) : divFn b ∈ FP := by + have := mem_FP_comp (mem_FP_comp (dmRun_mem_FP b) Cobham.fstBlock_mem_FP) + Cobham.fstBlock_mem_FP + simpa using this + +theorem modFn_mem_FP (b : List Bool) : modFn b ∈ FP := by + have := mem_FP_comp (mem_FP_comp (dmRun_mem_FP b) Cobham.fstBlock_mem_FP) + Cobham.sndBlock_mem_FP + simpa using this + +theorem divFn_eq {b : List Bool} (hb : 0 < b.length) (s : List Bool) : + divFn b s = List.replicate (s.length / b.length) true := by + rw [divFn, dmRun, dmStep_iterate hb s.length, pairFst_pair, pairFst_pair] + +theorem modFn_eq {b : List Bool} (hb : 0 < b.length) (s : List Bool) : + modFn b s = List.replicate (s.length % b.length) true := by + rw [modFn, dmRun, dmStep_iterate hb s.length, pairFst_pair, pairSnd_pair] + +/-! ### Dividing by a length read from the input -/ + +theorem fstBlock_len_le (z : List Bool) : (pairFst z).length ≤ z.length := by + induction z using pairFst.induct <;> simp [pairFst] <;> omega + +theorem sndBlock_len_le (z : List Bool) : (pairSnd z).length ≤ z.length := by + rcases hu : unpair? z with _ | ⟨p, q⟩ + · rw [show pairSnd z = [] from by rw [pairSnd, hu]] + simp + · have hz : z = pair p q := unpair?_eq_some_iff.mp hu + rw [show pairSnd z = q from by rw [pairSnd, hu], hz, pair_length] + omega + +/-- The counting run with the divisor read from the argument: `pair b s`. -/ +noncomputable def dmRun2 (z : List Bool) : List Bool := + dmStep^[(pairSnd z).length] (pair (pair [] []) (pairFst z)) + +/-- The quotient of one length by another, in unary. -/ +noncomputable def divFn2 (z : List Bool) : List Bool := + pairFst (pairFst (dmRun2 z)) + +/-- The remainder of one length by another, in unary. -/ +noncomputable def modFn2 (z : List Bool) : List Bool := + pairSnd (pairFst (dmRun2 z)) + +theorem dmRun2_mem_FP : dmRun2 ∈ FP := by + have hinit : (fun z : List Bool => pair (pair [] []) (pairFst z)) ∈ FP := + Cobham.pairFn_mem_FP (constFn_mem_FP (pair [] [])) Cobham.fstBlock_mem_FP + have hwidth : (fun z : List Bool => polyRuler (Polynomial.C 7 * Polynomial.X + + Polynomial.C 6) (id z)) ∈ FP := polyRulerFn_mem_FP _ id_mem_FP + have hbound : ∀ z : List Bool, ∀ k ≤ (pairSnd z).length, + (dmStep^[k] (pair (pair [] []) (pairFst z))).length + ≤ (polyRuler (Polynomial.C 7 * Polynomial.X + Polynomial.C 6) (id z)).length := by + intro z k hk + obtain ⟨q', r', h1, hq, hr⟩ := dmStep_shape k [] [] (pairFst z) + have hf : (pairFst z).length ≤ z.length := fstBlock_len_le z + have hs : (pairSnd z).length ≤ z.length := sndBlock_len_le z + rw [h1, pair_length, pair_length, polyRuler_length] + simp only [Polynomial.eval_add, Polynomial.eval_mul, Polynomial.eval_C, + Polynomial.eval_X, id, List.length_nil, Nat.zero_add] at * + omega + have hiter := Cobham.iterate_mem_FP dmStep_mem_FP hinit Cobham.sndBlock_mem_FP hwidth hbound + simpa using hiter + +theorem divFn2_mem_FP : divFn2 ∈ FP := by + have := mem_FP_comp (mem_FP_comp dmRun2_mem_FP Cobham.fstBlock_mem_FP) + Cobham.fstBlock_mem_FP + simpa using this + +theorem modFn2_mem_FP : modFn2 ∈ FP := by + have := mem_FP_comp (mem_FP_comp dmRun2_mem_FP Cobham.fstBlock_mem_FP) + Cobham.sndBlock_mem_FP + simpa using this + +theorem divFn2_eq {b : List Bool} (hb : 0 < b.length) (s : List Bool) : + divFn2 (pair b s) = List.replicate (s.length / b.length) true := by + rw [divFn2, dmRun2, pairSnd_pair, pairFst_pair, dmStep_iterate hb s.length, + pairFst_pair, pairFst_pair] + +theorem modFn2_eq {b : List Bool} (hb : 0 < b.length) (s : List Bool) : + modFn2 (pair b s) = List.replicate (s.length % b.length) true := by + rw [modFn2, dmRun2, pairSnd_pair, pairFst_pair, dmStep_iterate hb s.length, + pairFst_pair, pairSnd_pair] + +/-- **Halving a length**, in unary. -/ +noncomputable def halfFn (s : List Bool) : List Bool := divFn [false, false] s + +theorem halfFn_mem_FP : halfFn ∈ FP := divFn_mem_FP _ + +theorem halfFn_eq (s : List Bool) : halfFn s = List.replicate (s.length / 2) true := by + rw [halfFn, divFn_eq (by simp)] + rfl + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/UnaryExp.lean b/Complexitylib/Classes/PCP/Internal/UnaryExp.lean new file mode 100644 index 00000000..28bf6586 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/UnaryExp.lean @@ -0,0 +1,115 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.P.Cobham.Internal +public import Complexitylib.Classes.P.Cobham.Internal.PolyRuler +public import Complexitylib.Asymptotics + +/-! +# Counting out `2 ^ r n` in unary + +A verifier using `r n` coins has `2 ^ r n` coin strings, and an algorithm that +has to look at all of them needs that many steps counted out somewhere. When +`r` is logarithmic the count is polynomial, so a polynomial-time function can +write it down — by starting from a single mark and doubling `r n` times. + +The hypothesis is the one `Constructible` supplies: `r n` itself is available in +unary in polynomial time. + +## Main definitions + +- `Complexity.dbl` — doubling a string + +## Main results + +- `Complexity.exists_poly_two_pow_of_bigO_log` — `2 ^ O(log n)` is polynomial +- `Complexity.unaryExp_mem_FP` — `2 ^ r n` marks, in polynomial time +-/ + +@[expose] public section + +namespace Complexity + +/-- Doubling: the string followed by itself. -/ +def dbl (s : List Bool) : List Bool := s ++ s + +theorem dbl_mem_FP : dbl ∈ FP := Cobham.appendFn_mem_FP id_mem_FP id_mem_FP + +theorem dbl_iterate (n : ℕ) : dbl^[n] [true] = List.replicate (2 ^ n) true := by + induction n with + | zero => rfl + | succ n ih => + rw [Function.iterate_succ_apply', ih, dbl, ← List.replicate_add] + congr 1 + rw [pow_succ] + omega + +open scoped Complexity in +/-- **A logarithmic exponent gives a polynomial.** This is what makes a +`O(log n)` randomness bound usable: the number of coin strings stays +polynomial. -/ +theorem exists_poly_two_pow_of_bigO_log {r : ℕ → ℕ} (h : r =O fun n => Nat.log 2 n) : + ∃ p : Polynomial ℕ, ∀ n, 2 ^ r n ≤ p.eval n := by + rw [BigO, Asymptotics.isBigO_iff] at h + obtain ⟨C, hC⟩ := h + rw [Filter.eventually_atTop] at hC + obtain ⟨N, hN⟩ := hC + refine ⟨Polynomial.X ^ ⌈C⌉₊ + + Polynomial.C ((Finset.range (N + 1)).sup fun n => 2 ^ r n), ?_⟩ + intro n + simp only [Polynomial.eval_add, Polynomial.eval_C, Polynomial.eval_pow, Polynomial.eval_X] + by_cases hn : n < N + 1 + · have : 2 ^ r n ≤ (Finset.range (N + 1)).sup fun n => 2 ^ r n := + Finset.le_sup (f := fun n => 2 ^ r n) (Finset.mem_range.mpr hn) + omega + · have hnN : N ≤ n := by omega + have hn0 : n ≠ 0 := by omega + have hb := hN n hnN + simp only [Real.norm_natCast] at hb + have hC_le : C ≤ (⌈C⌉₊ : ℝ) := Nat.le_ceil C + have hlog_nonneg : (0 : ℝ) ≤ ((Nat.log 2 n : ℕ) : ℝ) := by positivity + have h_real : (r n : ℝ) ≤ (⌈C⌉₊ : ℝ) * ((Nat.log 2 n : ℕ) : ℝ) := + le_trans hb (mul_le_mul_of_nonneg_right hC_le hlog_nonneg) + have h_nat : r n ≤ ⌈C⌉₊ * Nat.log 2 n := by exact_mod_cast h_real + have h1 : 2 ^ r n ≤ 2 ^ (⌈C⌉₊ * Nat.log 2 n) := Nat.pow_le_pow_right (by omega) h_nat + have h2 : 2 ^ (⌈C⌉₊ * Nat.log 2 n) = (2 ^ Nat.log 2 n) ^ ⌈C⌉₊ := by + rw [← pow_mul, Nat.mul_comm] + have h3 : (2 ^ Nat.log 2 n) ^ ⌈C⌉₊ ≤ n ^ ⌈C⌉₊ := + Nat.pow_le_pow_left (Nat.pow_log_le_self 2 hn0) _ + omega + +/-- **Writing `2 ^ r n` marks.** If the number of coins is available in unary in +polynomial time and the number of coin strings is polynomially bounded, then +that many marks can be written in polynomial time. -/ +theorem unaryExp_mem_FP {r : ℕ → ℕ} + (hr : (fun x : List Bool => List.replicate (r x.length) true) ∈ FP) + (p : Polynomial ℕ) (hp : ∀ n, 2 ^ r n ≤ p.eval n) : + (fun x : List Bool => List.replicate (2 ^ r x.length) true) ∈ FP := by + have hwidth : (fun z : List Bool => polyRuler p (id z)) ∈ FP := + polyRulerFn_mem_FP p id_mem_FP + have hbound : ∀ z : List Bool, ∀ n ≤ (List.replicate (r z.length) true).length, + (dbl^[n] [true]).length ≤ (polyRuler p (id z)).length := by + intro z n hn + rw [List.length_replicate] at hn + rw [dbl_iterate, List.length_replicate, polyRuler_length] + exact le_trans (Nat.pow_le_pow_right (by omega) hn) (hp z.length) + have hiter := Cobham.iterate_mem_FP dbl_mem_FP (constFn_mem_FP [true]) hr hwidth hbound + refine mem_FP_of_eq hiter fun x => ?_ + rw [List.length_replicate, dbl_iterate] + +open scoped Complexity in +/-- **The form the assembly uses.** A constructible logarithmic randomness bound +lets the number of coin strings be counted out in polynomial time. -/ +theorem unaryExp_mem_FP_of_bigO_log {r : ℕ → ℕ} + (hr : (fun x : List Bool => List.replicate (r x.length) true) ∈ FP) + (h : r =O fun n => Nat.log 2 n) : + (fun x : List Bool => List.replicate (2 ^ r x.length) true) ∈ FP := by + obtain ⟨p, hp⟩ := exists_poly_two_pow_of_bigO_log h + exact unaryExp_mem_FP hr p hp + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/UnaryList.lean b/Complexitylib/Classes/PCP/Internal/UnaryList.lean new file mode 100644 index 00000000..e1e07b79 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/UnaryList.lean @@ -0,0 +1,420 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.HeadOps +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.P +public import Complexitylib.Classes.PCP.Internal.PosScan +public import Complexitylib.Classes.PCP.Internal.UnaryDivMod +public import Complexitylib.Classes.PCP.Internal.NatEncode + +/-! +# Reading a table of unary numbers + +An algorithm that materializes a graph writes a list of records and reads them +back. `PosScan` reads an entry of an encoded list, and `DataEncode` writes the +entries; what is missing is getting a *number* back out, in the unary form the +loops of the toolkit consume. + +Storing the number in unary makes that a length computation: the encoding of a +unary string of `w` marks is `4 * w + 2` bits long — two brackets, and four bits +a mark — so dividing the length by four recovers the marks. No parsing of the +encoding is needed, and no binary arithmetic. + +The same scan reads the two halves of an encoded pair, since a pair is encoded +as the two-element list of its halves. + +## Main definitions + +- `Complexity.unaryOf` — the marks an encoded unary string stands for +- `Complexity.fstEnc`, `Complexity.sndEnc` — the halves of an encoded pair + +## Main results + +- `Complexity.unaryOf_encode`, `Complexity.unaryOf_mem_FP` +- `Complexity.fstEnc_eq`, `Complexity.sndEnc_eq`, and their `FP` versions +- `Complexity.tableFst_eq`, `Complexity.tableSnd_eq` — an entry of a table of + pairs of unary numbers, read back in unary +- `Complexity.recFst_eq`, `Complexity.recSnd_eq`, `Complexity.recThd_eq` — the + same for records of three numbers +- `Complexity.encPair_eq` — and written out +-/ + +@[expose] public section + +namespace Complexity + +/-! ### Numbers -/ + +/-- The size of an encoded unary string: two brackets and four bits a mark. -/ +theorem size_encode_replicate (w : ℕ) : + (DataEncode.encode (List.replicate w true)).size = 4 * w + 2 := by + have hone : (DataEncode.encode true).size = 4 := by + show (Data.l [Data.l []]).size = 4 + rw [Data.cons_size] + simp + induction w with + | zero => simp + | succ w ih => + have h : DataEncode.encode (List.replicate (w + 1) true) + = Data.l (DataEncode.encode true :: (List.replicate w true).map DataEncode.encode) := by + show Data.l ((List.replicate (w + 1) true).map DataEncode.encode) = _ + rw [List.replicate_succ, List.map_cons] + rw [h, Data.cons_size, hone, + show (Data.l ((List.replicate w true).map DataEncode.encode)).size + = (DataEncode.encode (List.replicate w true)).size from rfl, ih] + omega + +theorem length_bitstringEncode_replicate (w : ℕ) : + (DataEncode.bitstringEncode (List.replicate w true)).length = 4 * w + 2 := by + rw [DataEncode.bitstringEncode_def, Data.length_toBits, size_encode_replicate] + +/-- The unary number an encoded unary string stands for. -/ +noncomputable def unaryOf (e : List Bool) : List Bool := + divFn [false, false, false, false] (dropOne (dropOne e)) + +theorem unaryOf_encode (w : ℕ) : + unaryOf (DataEncode.bitstringEncode (List.replicate w true)) = List.replicate w true := by + rw [unaryOf, divFn_eq (by norm_num)] + congr 1 + have hlen : (dropOne (dropOne + (DataEncode.bitstringEncode (List.replicate w true)))).length = 4 * w := by + rw [dropOne, dropOne, List.length_drop, List.length_drop, + length_bitstringEncode_replicate] + omega + rw [hlen] + norm_num + +theorem unaryOf_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => unaryOf (a z)) ∈ FP := by + have h := mem_FP_comp (dropOneFn_mem_FP (dropOneFn_mem_FP ha)) + (divFn_mem_FP [false, false, false, false]) + exact mem_FP_of_eq h fun z => rfl + +/-! ### Pairs -/ + +/-- The first half of an encoded pair. -/ +noncomputable def fstEnc (e : List Bool) : List Bool := posAt e 0 + +/-- The second half of an encoded pair. -/ +noncomputable def sndEnc (e : List Bool) : List Bool := posAt e 1 + +theorem bitstringEncode_prod {α β : Type} [DataEncode α] [DataEncode β] (a : α) (b : β) : + DataEncode.bitstringEncode (a, b) + = DataEncode.bitstringEncode + ([DataEncode.encode a, DataEncode.encode b] : List Data) := by + rw [DataEncode.bitstringEncode_def, DataEncode.bitstringEncode_def, DataEncode_pair] + show _ = (Data.l (([DataEncode.encode a, DataEncode.encode b] : List Data).map id)).toBits + rw [List.map_id] + +theorem fstEnc_eq {α β : Type} [DataEncode α] [DataEncode β] (a : α) (b : β) : + fstEnc (DataEncode.bitstringEncode (a, b)) = DataEncode.bitstringEncode a := by + rw [fstEnc, bitstringEncode_prod, + posAt_eq_of_lt (l := ([DataEncode.encode a, DataEncode.encode b] : List Data)) + (by norm_num)] + rfl + +theorem sndEnc_eq {α β : Type} [DataEncode α] [DataEncode β] (a : α) (b : β) : + sndEnc (DataEncode.bitstringEncode (a, b)) = DataEncode.bitstringEncode b := by + rw [sndEnc, bitstringEncode_prod, + posAt_eq_of_lt (l := ([DataEncode.encode a, DataEncode.encode b] : List Data)) + (by norm_num)] + rfl + +theorem fstEnc_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => fstEnc (a z)) ∈ FP := by + have h := posAt_mem_FP (constFn_mem_FP ([] : List Bool)) ha + exact mem_FP_of_eq h fun z => rfl + +theorem sndEnc_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => sndEnc (a z)) ∈ FP := by + have h := posAt_mem_FP (constFn_mem_FP ([true] : List Bool)) ha + exact mem_FP_of_eq h fun z => rfl + +/-! ### Unary arithmetic with constants -/ + +/-- Any string, as that many marks. -/ +noncomputable def marks (s : List Bool) : List Bool := divFn [false] s + +theorem marks_eq (s : List Bool) : marks s = List.replicate s.length true := by + rw [marks, divFn_eq (by norm_num)] + simp + +theorem marks_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => marks (a z)) ∈ FP := + mem_FP_of_eq (mem_FP_comp ha (divFn_mem_FP [false])) fun _ => rfl + +/-- Division by a constant. -/ +noncomputable def divC (c : ℕ) (s : List Bool) : List Bool := + divFn (List.replicate c false) s + +theorem divC_eq {c : ℕ} (hc : 0 < c) (s : List Bool) : + divC c s = List.replicate (s.length / c) true := by + rw [divC, divFn_eq (by simpa using hc), List.length_replicate] + +theorem divC_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) (c : ℕ) : + (fun z => divC c (a z)) ∈ FP := + mem_FP_of_eq (mem_FP_comp ha (divFn_mem_FP _)) fun _ => rfl + +/-- Remainder by a constant. -/ +noncomputable def modC (c : ℕ) (s : List Bool) : List Bool := + modFn (List.replicate c false) s + +theorem modC_eq {c : ℕ} (hc : 0 < c) (s : List Bool) : + modC c s = List.replicate (s.length % c) true := by + rw [modC, modFn_eq (by simpa using hc), List.length_replicate] + +theorem modC_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) (c : ℕ) : + (fun z => modC c (a z)) ∈ FP := + mem_FP_of_eq (mem_FP_comp ha (modFn_mem_FP _)) fun _ => rfl + +/-- The product of two lengths. -/ +def mulLen (a b : List Bool) : List Bool := List.replicate (a.length * b.length) false + +@[simp] theorem length_mulLen (a b : List Bool) : + (mulLen a b).length = a.length * b.length := by + rw [mulLen, List.length_replicate] + +theorem mulLen_mem_FP {f g : List Bool → List Bool} (hf : f ∈ FP) (hg : g ∈ FP) : + (fun z => mulLen (f z) (g z)) ∈ FP := + Cobham.mulLenFn_mem_FP hf hg + +/-- Multiplication by a constant, as a length. -/ +def mulC (c : ℕ) (s : List Bool) : List Bool := List.replicate (s.length * c) false + +@[simp] theorem length_mulC (c : ℕ) (s : List Bool) : (mulC c s).length = s.length * c := by + rw [mulC, List.length_replicate] + +theorem mulC_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) (c : ℕ) : + (fun z => mulC c (a z)) ∈ FP := by + have := Cobham.mulLenFn_mem_FP ha (constFn_mem_FP (List.replicate c false)) + refine mem_FP_of_eq this fun z => ?_ + rw [mulC, List.length_replicate] + +/-! ### Writing records -/ + +/-- The encoding of a unary string. -/ +def encUnary (s : List Bool) : List Bool := false :: s.flatMap boolBits ++ [true] + +theorem encUnary_eq (s : List Bool) : encUnary s = DataEncode.bitstringEncode s := + (bitstringEncode_list s).symm + +theorem encUnary_mem_FP {a : List Bool → List Bool} (ha : a ∈ FP) : + (fun z => encUnary (a z)) ∈ FP := by + have hflat : (fun z => (a z).flatMap boolBits) ∈ FP := by + have hpair : (fun z => pair z (a z)) ∈ FP := Cobham.pairFn_mem_FP id_mem_FP ha + have := mem_FP_comp hpair flatBitsFn_mem_FP + refine mem_FP_of_eq this fun z => ?_ + rw [Function.comp_apply, flatBitsFn_eq, pairSnd_pair] + have hcons := mem_FP_comp hflat (Cobham.cons_mem_FP false) + have := Cobham.appendFn_mem_FP hcons (constFn_mem_FP [true]) + exact mem_FP_of_eq this fun z => rfl + +/-- The encoding of a pair of unary strings. -/ +def encPair (a b : List Bool) : List Bool := false :: (encUnary a ++ encUnary b) ++ [true] + +theorem bitstringEncode_prod_eq {α β : Type} [DataEncode α] [DataEncode β] (a : α) (b : β) : + DataEncode.bitstringEncode ((a, b) : α × β) + = false :: (DataEncode.bitstringEncode a ++ DataEncode.bitstringEncode b) ++ [true] := by + rw [DataEncode.bitstringEncode_def, DataEncode_pair, Data.toBits_l] + simp [DataEncode.bitstringEncode_def] + +theorem encPair_eq (a b : List Bool) : + encPair a b = DataEncode.bitstringEncode ((a, b) : List Bool × List Bool) := by + rw [bitstringEncode_prod_eq, encPair, encUnary_eq, encUnary_eq] + +theorem encPair_mem_FP {a b : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) : + (fun z => encPair (a z) (b z)) ∈ FP := by + have happ := Cobham.appendFn_mem_FP (encUnary_mem_FP ha) (encUnary_mem_FP hb) + have hcons := mem_FP_comp happ (Cobham.cons_mem_FP false) + have := Cobham.appendFn_mem_FP hcons (constFn_mem_FP [true]) + exact mem_FP_of_eq this fun z => rfl + +theorem unaryOf_fstEnc_encPair (w c : ℕ) : + unaryOf (fstEnc (encPair (List.replicate w true) (List.replicate c true))) + = List.replicate w true := by + rw [encPair_eq, fstEnc_eq, unaryOf_encode] + +theorem unaryOf_sndEnc_encPair (w c : ℕ) : + unaryOf (sndEnc (encPair (List.replicate w true) (List.replicate c true))) + = List.replicate c true := by + rw [encPair_eq, sndEnc_eq, unaryOf_encode] + +@[simp] theorem marks_append_mulC (w m c : ℕ) : + marks (List.replicate w true ++ mulC c (List.replicate m true)) + = List.replicate (w + m * c) true := by + rw [marks_eq, List.length_append, List.length_replicate, length_mulC, + List.length_replicate] + +theorem length_mulC_append (m c w : ℕ) : + (mulC c (List.replicate m true) ++ List.replicate w true).length = m * c + w := by + rw [List.length_append, length_mulC, List.length_replicate, List.length_replicate] + +/-- The encoding of a list is two brackets and its entries' encodings. -/ +theorem length_bitstringEncode_list {α : Type} [DataEncode α] (l : List α) : + (DataEncode.bitstringEncode l).length + = 2 + (l.map fun a => (DataEncode.bitstringEncode a).length).sum := by + induction l with + | nil => simp [DataEncode.bitstringEncode_def] + | cons a l ih => + have hcons : DataEncode.encode (a :: l) + = Data.l (DataEncode.encode a :: l.map DataEncode.encode) := rfl + have htail : DataEncode.encode l = Data.l (l.map DataEncode.encode) := rfl + rw [DataEncode.bitstringEncode_def, hcons, Data.length_toBits, Data.cons_size, + ← htail, ← Data.length_toBits, ← Data.length_toBits, + ← DataEncode.bitstringEncode_def, ← DataEncode.bitstringEncode_def, ih] + simp + omega + +@[simp] theorem length_encPair (w c : ℕ) : + (encPair (List.replicate w true) (List.replicate c true)).length = 4 * w + 4 * c + 6 := by + rw [encPair_eq, ← encPair_eq, encPair, encUnary_eq, encUnary_eq] + simp only [List.length_append, List.length_cons, List.length_nil, + length_bitstringEncode_replicate] + omega + +/-! ### Tables of pairs of numbers -/ + +/-- The first number of the `j`-th record of a table, in unary. -/ +noncomputable def tableFst (T : List Bool) (j : ℕ) : List Bool := unaryOf (fstEnc (posAt T j)) + +/-- The second number of the `j`-th record of a table, in unary. -/ +noncomputable def tableSnd (T : List Bool) (j : ℕ) : List Bool := unaryOf (sndEnc (posAt T j)) + +variable {l : List (List Bool × List Bool)} {j w c : ℕ} + +theorem tableFst_def (T : List Bool) (j : ℕ) : + tableFst T j = unaryOf (fstEnc (posAt T j)) := rfl + +theorem tableSnd_def (T : List Bool) (j : ℕ) : + tableSnd T j = unaryOf (sndEnc (posAt T j)) := rfl + +theorem tableFst_eq (hj : j < l.length) + (h : l[j]'hj = (List.replicate w true, List.replicate c true)) : + tableFst (DataEncode.bitstringEncode l) j = List.replicate w true := by + rw [tableFst, posAt_eq_of_lt hj, h, fstEnc_eq, unaryOf_encode] + +theorem tableSnd_eq (hj : j < l.length) + (h : l[j]'hj = (List.replicate w true, List.replicate c true)) : + tableSnd (DataEncode.bitstringEncode l) j = List.replicate c true := by + rw [tableSnd, posAt_eq_of_lt hj, h, sndEnc_eq, unaryOf_encode] + +theorem tableFst_mem_FP {a b : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) : + (fun z => tableFst (b z) (a z).length) ∈ FP := + unaryOf_mem_FP (fstEnc_mem_FP (posAt_mem_FP ha hb)) + +theorem tableSnd_mem_FP {a b : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) : + (fun z => tableSnd (b z) (a z).length) ∈ FP := + unaryOf_mem_FP (sndEnc_mem_FP (posAt_mem_FP ha hb)) + +/-! ### Records of three numbers -/ + +/-- The encoding of three unary strings. -/ +def encTriple (a b c : List Bool) : List Bool := false :: (encUnary a ++ encPair b c) ++ [true] + +theorem encTriple_eq (a b c : List Bool) : + encTriple a b c + = DataEncode.bitstringEncode ((a, (b, c)) : List Bool × List Bool × List Bool) := by + rw [bitstringEncode_prod_eq, encTriple, encUnary_eq, ← encPair_eq] + +@[simp] theorem length_encTriple (a b c : ℕ) : + (encTriple (List.replicate a true) (List.replicate b true) + (List.replicate c true)).length = 4 * a + 4 * b + 4 * c + 10 := by + rw [encTriple, encUnary_eq] + simp only [List.length_append, List.length_cons, List.length_nil, + length_bitstringEncode_replicate, length_encPair] + omega + +theorem encTriple_mem_FP {a b c : List Bool → List Bool} (ha : a ∈ FP) (hb : b ∈ FP) + (hc : c ∈ FP) : (fun z => encTriple (a z) (b z) (c z)) ∈ FP := by + have happ := Cobham.appendFn_mem_FP (encUnary_mem_FP ha) (encPair_mem_FP hb hc) + have hcons := mem_FP_comp happ (Cobham.cons_mem_FP false) + have := Cobham.appendFn_mem_FP hcons (constFn_mem_FP [true]) + exact mem_FP_of_eq this fun _ => rfl + +/-- The first number of the `j`-th record of a table of triples. -/ +noncomputable def recFst (T : List Bool) (j : ℕ) : List Bool := unaryOf (fstEnc (posAt T j)) + +/-- The second. -/ +noncomputable def recSnd (T : List Bool) (j : ℕ) : List Bool := + unaryOf (fstEnc (sndEnc (posAt T j))) + +/-- The third. -/ +noncomputable def recThd (T : List Bool) (j : ℕ) : List Bool := + unaryOf (sndEnc (sndEnc (posAt T j))) + +variable {l3 : List (List Bool × List Bool × List Bool)} {a b c : ℕ} + +theorem recFst_eq (hj : j < l3.length) + (h : l3[j]'hj = (List.replicate a true, List.replicate b true, List.replicate c true)) : + recFst (DataEncode.bitstringEncode l3) j = List.replicate a true := by + rw [recFst, posAt_eq_of_lt hj, h, fstEnc_eq, unaryOf_encode] + +theorem recSnd_eq (hj : j < l3.length) + (h : l3[j]'hj = (List.replicate a true, List.replicate b true, List.replicate c true)) : + recSnd (DataEncode.bitstringEncode l3) j = List.replicate b true := by + rw [recSnd, posAt_eq_of_lt hj, h, sndEnc_eq, fstEnc_eq, unaryOf_encode] + +theorem recThd_eq (hj : j < l3.length) + (h : l3[j]'hj = (List.replicate a true, List.replicate b true, List.replicate c true)) : + recThd (DataEncode.bitstringEncode l3) j = List.replicate c true := by + rw [recThd, posAt_eq_of_lt hj, h, sndEnc_eq, sndEnc_eq, unaryOf_encode] + +theorem recFst_mem_FP {f g : List Bool → List Bool} (hf : f ∈ FP) (hg : g ∈ FP) : + (fun z => recFst (g z) (f z).length) ∈ FP := + unaryOf_mem_FP (fstEnc_mem_FP (posAt_mem_FP hf hg)) + +theorem recSnd_mem_FP {f g : List Bool → List Bool} (hf : f ∈ FP) (hg : g ∈ FP) : + (fun z => recSnd (g z) (f z).length) ∈ FP := + unaryOf_mem_FP (fstEnc_mem_FP (sndEnc_mem_FP (posAt_mem_FP hf hg))) + +theorem recThd_mem_FP {f g : List Bool → List Bool} (hf : f ∈ FP) (hg : g ∈ FP) : + (fun z => recThd (g z) (f z).length) ∈ FP := + unaryOf_mem_FP (sndEnc_mem_FP (sndEnc_mem_FP (posAt_mem_FP hf hg))) + +/-! ### Digit sums -/ + +/-- A number from its digits: `∑ j < n, digit j · radix ^ j`, written in marks. +The digits are read from the input, so this is how an algorithm assembles a +mixed-radix number out of constantly many pieces. -/ +noncomputable def digitSum (radix : ℕ) (digit : ℕ → List Bool → List Bool) : + ℕ → List Bool → List Bool + | 0, _ => [] + | n + 1, w => digitSum radix digit n w ++ mulC (radix ^ n) (digit n w) + +theorem digitSum_mem_FP {radix : ℕ} {digit : ℕ → List Bool → List Bool} + (hd : ∀ i, digit i ∈ FP) : ∀ n, digitSum radix digit n ∈ FP := by + intro n + induction n with + | zero => exact mem_FP_of_eq (constFn_mem_FP []) fun w => by rw [digitSum] + | succ n ih => + refine mem_FP_of_eq (Cobham.appendFn_mem_FP ih + (mulC_mem_FP (hd n) (radix ^ n))) fun w => ?_ + rw [digitSum] + +@[simp] theorem length_digitSum (radix : ℕ) (digit : ℕ → List Bool → List Bool) + (w : List Bool) : ∀ n, (digitSum radix digit n w).length + = ∑ j ∈ Finset.range n, (digit j w).length * radix ^ j := by + intro n + induction n with + | zero => rw [digitSum, Finset.range_zero, Finset.sum_empty, List.length_nil] + | succ n ih => + rw [digitSum, List.length_append, ih, Finset.sum_range_succ, length_mulC] + +theorem length_digitSum_le {radix : ℕ} (hr : 0 < radix) + {digit : ℕ → List Bool → List Bool} {d : ℕ} (hd : ∀ j w, (digit j w).length ≤ d) + (n : ℕ) (w : List Bool) : + (digitSum radix digit n w).length ≤ n * (d * radix ^ n) := by + rw [length_digitSum] + calc ∑ j ∈ Finset.range n, (digit j w).length * radix ^ j + ≤ ∑ _j ∈ Finset.range n, d * radix ^ n := by + refine Finset.sum_le_sum fun j hj => ?_ + rw [Finset.mem_range] at hj + exact Nat.mul_le_mul (hd j w) (Nat.pow_le_pow_right hr (le_of_lt hj)) + _ = n * (d * radix ^ n) := by + rw [Finset.sum_const, Finset.card_range, smul_eq_mul] + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/Union.lean b/Complexitylib/Classes/PCP/Internal/Union.lean new file mode 100644 index 00000000..8bde9933 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/Union.lean @@ -0,0 +1,212 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.RegularGraph + +/-! +# Superposing two graphs, and expanderization + +Dinur's preprocessing turns an arbitrary regular constraint graph into one that +is also an *expander*, by laying an expander over the same vertex set and taking +the union of the two edge sets. This module builds that union and bounds its +spectral gap. + +The union of a graph `G` with a graph `H` — identified with `G`'s vertices along +a bijection `e` — has label type `G.D ⊕ H.D` and degree `G.deg + H.deg`. Its +walk operator is the weighted average of the two: + +`step_U f = (G.deg • step_G f + H.deg • step_H (f ∘ e)) / (G.deg + H.deg)` + +so if `H` contracts mean-zero functions by `lam`, the union contracts them by +`(G.deg + H.deg * lam) / (G.deg + H.deg)` — strictly below one whenever `lam` is +— using nothing about `G` beyond `sum_sq_step_le`, that its own walk operator is +a contraction. + +## Staying square-root-free + +The triangle inequality looks unavailable in the squared-norm formulation, but +it is not needed. Expanding `‖aX + bY‖²` leaves a cross term `⟨X, Y⟩`, and +Cauchy–Schwarz bounds its *square* by `‖X‖² ‖Y‖² ≤ A · lam² A = (lam A)²` — +whose square root, `lam A`, is rational in the data. So the cross term is +bounded with one application of `le_of_sq_le_sq` and no `Real.sqrt` ever +appears. + +## Main definitions + +- `RegGraph.unionRot` — the rotation map of the union +- `RegGraph.union` — the union graph + +## Main results + +- `RegGraph.deg_union`, `RegGraph.step_union` +- `RegGraph.sum_sq_step_union_le` — the combined contraction bound +- `RegGraph.spectralBound_union` — expanderization: laying an expander over any + regular graph gives a spectral bound strictly below one +-/ + +@[expose] public section + +namespace Complexity + +namespace RegGraph + +variable (G H : RegGraph) (e : H.V ≃ G.V) + +/-- The rotation map of the union: reverse within `G`, or within `H` after +transporting along `e`. -/ +def unionRot : G.V × (G.D ⊕ H.D) → G.V × (G.D ⊕ H.D) + | (v, Sum.inl i) => ((G.rot (v, i)).1, Sum.inl (G.rot (v, i)).2) + | (v, Sum.inr j) => (e (H.rot (e.symm v, j)).1, Sum.inr (H.rot (e.symm v, j)).2) + +theorem unionRot_involutive : Function.Involutive (unionRot G H e) := by + rintro ⟨v, i | j⟩ + · show ((G.rot ((G.rot (v, i)).1, (G.rot (v, i)).2)).1, + Sum.inl (G.rot ((G.rot (v, i)).1, (G.rot (v, i)).2)).2) = (v, Sum.inl i) + rw [Prod.mk.eta, G.rot_involutive (v, i)] + · show (e (H.rot (e.symm (e (H.rot (e.symm v, j)).1), (H.rot (e.symm v, j)).2)).1, + Sum.inr (H.rot (e.symm (e (H.rot (e.symm v, j)).1), (H.rot (e.symm v, j)).2)).2) + = (v, Sum.inr j) + rw [Equiv.symm_apply_apply, Prod.mk.eta, H.rot_involutive (e.symm v, j)] + simp + +/-- The union of `G` with `H`, whose vertices are identified with `G`'s along +`e`: the edge sets are superposed. -/ +def union : RegGraph where + V := G.V + D := G.D ⊕ H.D + decEqV := G.decEqV + decEqD := inferInstance + fintypeV := G.fintypeV + fintypeD := inferInstance + nonemptyD := ⟨Sum.inl (Classical.arbitrary G.D)⟩ + rot := unionRot G H e + rot_involutive := unionRot_involutive G H e + +@[simp] theorem V_union : (union G H e).V = G.V := rfl + +@[simp] theorem order_union : (union G H e).order = G.order := rfl + +@[simp] theorem deg_union : (union G H e).deg = G.deg + H.deg := by + show Fintype.card (G.D ⊕ H.D) = G.deg + H.deg + rw [Fintype.card_sum] + rfl + +/-- The union's walk operator is the degree-weighted average of the two walk +operators, the second read through the identification `e`. -/ +theorem step_union (f : G.V → ℝ) (v : G.V) : + (union G H e).step f v + = ((G.deg : ℝ) * G.step f v + (H.deg : ℝ) * H.step (fun u => f (e u)) (e.symm v)) + / ((G.deg : ℝ) + (H.deg : ℝ)) := by + have hdG : (G.deg : ℝ) ≠ 0 := G.deg_ne_zero + have hdH : (H.deg : ℝ) ≠ 0 := H.deg_ne_zero + have hsum : ∑ i : (union G H e).D, f ((union G H e).nbr v i) + = (∑ i : G.D, f (G.nbr v i)) + ∑ j : H.D, f (e (H.nbr (e.symm v) j)) := by + show ∑ i : G.D ⊕ H.D, f ((union G H e).nbr v i) = _ + rw [Fintype.sum_sum_type] + rfl + rw [step, hsum, deg_union] + push_cast + rw [step, step] + field_simp + +/-! ### The combined contraction bound -/ + +/-- The cross term in the expansion of the union's walk operator, bounded by +Cauchy–Schwarz without any square root. -/ +private theorem sum_mul_le_of_sq_le {X Y : G.V → ℝ} {A lam : ℝ} (hlam : 0 ≤ lam) + (hA : 0 ≤ A) (hX : (∑ v : G.V, (X v) ^ 2) ≤ A) + (hY : (∑ v : G.V, (Y v) ^ 2) ≤ lam ^ 2 * A) : + (∑ v : G.V, X v * Y v) ≤ lam * A := by + have hcs : (∑ v : G.V, X v * Y v) ^ 2 + ≤ (∑ v : G.V, (X v) ^ 2) * ∑ v : G.V, (Y v) ^ 2 := + Finset.sum_mul_sq_le_sq_mul_sq _ _ _ + have hprod : (∑ v : G.V, (X v) ^ 2) * (∑ v : G.V, (Y v) ^ 2) ≤ (lam * A) ^ 2 := by + have h2 : (0 : ℝ) ≤ ∑ v : G.V, (Y v) ^ 2 := Finset.sum_nonneg fun _ _ => sq_nonneg _ + calc (∑ v : G.V, (X v) ^ 2) * (∑ v : G.V, (Y v) ^ 2) + ≤ A * (lam ^ 2 * A) := by + exact mul_le_mul hX hY h2 hA + _ = (lam * A) ^ 2 := by ring + exact le_of_sq_le_sq (le_trans hcs hprod) (by positivity) + +/-- **Expanderization, quantitatively.** If `H` contracts mean-zero functions by +`lam`, then the union contracts them by the degree-weighted average of `1` and +`lam`. -/ +theorem sum_sq_step_union_le {lam : ℝ} (hlam : 0 ≤ lam) (hH : H.SpectralBound lam) + (f : G.V → ℝ) (hf : (∑ v : G.V, f v) = 0) : + (∑ v : G.V, ((union G H e).step f v) ^ 2) + ≤ (((G.deg : ℝ) + (H.deg : ℝ) * lam) / ((G.deg : ℝ) + (H.deg : ℝ))) ^ 2 + * ∑ v : G.V, (f v) ^ 2 := by + set A : ℝ := ∑ v : G.V, (f v) ^ 2 with hAdef + have hA : 0 ≤ A := Finset.sum_nonneg fun _ _ => sq_nonneg _ + have hdG : (0 : ℝ) < (G.deg : ℝ) := by have := G.deg_pos; positivity + set X : G.V → ℝ := fun v => G.step f v with hXdef + set Y : G.V → ℝ := fun v => H.step (fun u => f (e u)) (e.symm v) with hYdef + -- the two individual bounds + have hX : (∑ v : G.V, (X v) ^ 2) ≤ A := G.sum_sq_step_le f + have hfe : (∑ u : H.V, f (e u)) = 0 := by + rw [Equiv.sum_comp e f, hf] + have hYtrans : (∑ v : G.V, (Y v) ^ 2) + = ∑ u : H.V, (H.step (fun u => f (e u)) u) ^ 2 := by + rw [← Equiv.sum_comp e (fun v => (H.step (fun u => f (e u)) (e.symm v)) ^ 2)] + exact Finset.sum_congr rfl fun u _ => by rw [Equiv.symm_apply_apply] + have hY : (∑ v : G.V, (Y v) ^ 2) ≤ lam ^ 2 * A := by + rw [hYtrans] + have h := hH (fun u => f (e u)) hfe + calc ∑ u : H.V, (H.step (fun u => f (e u)) u) ^ 2 + ≤ lam ^ 2 * ∑ u : H.V, (f (e u)) ^ 2 := h + _ = lam ^ 2 * A := by rw [hAdef, Equiv.sum_comp e (fun v => (f v) ^ 2)] + have hcross : (∑ v : G.V, X v * Y v) ≤ lam * A := + sum_mul_le_of_sq_le G hlam hA hX hY + -- expand the union's operator + have hexp : ∀ v : G.V, ((union G H e).step f v) ^ 2 + = ((G.deg : ℝ) ^ 2 * (X v) ^ 2 + 2 * (G.deg : ℝ) * (H.deg : ℝ) * (X v * Y v) + + (H.deg : ℝ) ^ 2 * (Y v) ^ 2) / ((G.deg : ℝ) + (H.deg : ℝ)) ^ 2 := by + intro v + rw [step_union] + field_simp + ring + have hden : (0 : ℝ) < ((G.deg : ℝ) + (H.deg : ℝ)) ^ 2 := by positivity + rw [Finset.sum_congr rfl fun v _ => hexp v, ← Finset.sum_div, div_le_iff₀ hden] + have hsplit : ∑ v : G.V, ((G.deg : ℝ) ^ 2 * (X v) ^ 2 + + 2 * (G.deg : ℝ) * (H.deg : ℝ) * (X v * Y v) + (H.deg : ℝ) ^ 2 * (Y v) ^ 2) + = (G.deg : ℝ) ^ 2 * (∑ v : G.V, (X v) ^ 2) + + 2 * (G.deg : ℝ) * (H.deg : ℝ) * (∑ v : G.V, X v * Y v) + + (H.deg : ℝ) ^ 2 * ∑ v : G.V, (Y v) ^ 2 := by + rw [Finset.sum_add_distrib, Finset.sum_add_distrib, ← Finset.mul_sum, ← Finset.mul_sum, + ← Finset.mul_sum] + rw [hsplit] + have hgoal : (G.deg : ℝ) ^ 2 * (∑ v : G.V, (X v) ^ 2) + + 2 * (G.deg : ℝ) * (H.deg : ℝ) * (∑ v : G.V, X v * Y v) + + (H.deg : ℝ) ^ 2 * (∑ v : G.V, (Y v) ^ 2) + ≤ (G.deg : ℝ) ^ 2 * A + 2 * (G.deg : ℝ) * (H.deg : ℝ) * (lam * A) + + (H.deg : ℝ) ^ 2 * (lam ^ 2 * A) := by + have h1 : (G.deg : ℝ) ^ 2 * (∑ v : G.V, (X v) ^ 2) ≤ (G.deg : ℝ) ^ 2 * A := by + exact mul_le_mul_of_nonneg_left hX (by positivity) + have h2 : 2 * (G.deg : ℝ) * (H.deg : ℝ) * (∑ v : G.V, X v * Y v) + ≤ 2 * (G.deg : ℝ) * (H.deg : ℝ) * (lam * A) := by + exact mul_le_mul_of_nonneg_left hcross (by positivity) + have h3 : (H.deg : ℝ) ^ 2 * (∑ v : G.V, (Y v) ^ 2) ≤ (H.deg : ℝ) ^ 2 * (lam ^ 2 * A) := by + exact mul_le_mul_of_nonneg_left hY (by positivity) + linarith + calc (G.deg : ℝ) ^ 2 * (∑ v : G.V, (X v) ^ 2) + + 2 * (G.deg : ℝ) * (H.deg : ℝ) * (∑ v : G.V, X v * Y v) + + (H.deg : ℝ) ^ 2 * (∑ v : G.V, (Y v) ^ 2) + ≤ (G.deg : ℝ) ^ 2 * A + 2 * (G.deg : ℝ) * (H.deg : ℝ) * (lam * A) + + (H.deg : ℝ) ^ 2 * (lam ^ 2 * A) := hgoal + _ = (((G.deg : ℝ) + (H.deg : ℝ) * lam) / ((G.deg : ℝ) + (H.deg : ℝ))) ^ 2 * A + * ((G.deg : ℝ) + (H.deg : ℝ)) ^ 2 := by + field_simp + ring + +/-- **Expanderization.** -/ +theorem spectralBound_union {lam : ℝ} (hlam : 0 ≤ lam) (hH : H.SpectralBound lam) : + (union G H e).SpectralBound + (((G.deg : ℝ) + (H.deg : ℝ) * lam) / ((G.deg : ℝ) + (H.deg : ℝ))) := + fun f hf => sum_sq_step_union_le G H e hlam hH f hf + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/VerifierLang.lean b/Complexitylib/Classes/PCP/Internal/VerifierLang.lean new file mode 100644 index 00000000..1a87bbbd --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/VerifierLang.lean @@ -0,0 +1,268 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.P.Cobham.Internal.FPBridge +public import Complexitylib.Classes.PCP.Internal.PosScan +public import Complexitylib.Classes.PCP.Internal.UnaryExp +public import Complexitylib.Classes.PCP.Internal.BoundedQuant + +/-! +# The witness layout of a PCP verifier + +A witness for a PCP verifier is a table of answers, one fixed-width block per +coin string. This module fixes that layout and reads it back: the block for a +coin string, cut down to the number of queries actually made, is exactly the +answer list `SubsetNP.tableOf` names. + +It also records that the query bound can be taken to be a *constant*. A bound +`q =O 1` need not be a computable function, but it is eventually bounded and +takes finitely many values before that, so a single number bounds every query +list — which is what an algorithm can actually use. + +## Main definitions + +- `Complexity.wBlock` — the witness block for one coin string + +## Main results + +- `Complexity.exists_const_query_bound` — a constant bounds every query list +- `Complexity.tableOf_eq_wBlock` — the block is the answer list +- `Complexity.accLang_mem_P` — checking every coin string is polynomial time +-/ + +@[expose] public section + +namespace Complexity + +open scoped Complexity in +/-- **A constant bounds every query list.** This is what makes the witness +layout uniform: each block has the same fixed width. -/ +theorem exists_const_query_bound {V : PCPVerifier} {q : ℕ → ℕ} + (hV : V.QueryBounded q) (hq : q =O fun _ => 1) : + ∃ K : ℕ, ∀ x ρ : List Bool, (V.positions x ρ).length ≤ K := by + rw [BigO, Asymptotics.isBigO_iff] at hq + obtain ⟨C, hC⟩ := hq + rw [Filter.eventually_atTop] at hC + obtain ⟨N, hN⟩ := hC + refine ⟨max ⌈C⌉₊ ((Finset.range (N + 1)).sup q), fun x ρ => ?_⟩ + refine le_trans (hV x ρ) ?_ + by_cases h : x.length < N + 1 + · exact le_trans (Finset.le_sup (f := q) (Finset.mem_range.mpr h)) (le_max_right _ _) + · have hb := hN x.length (by omega) + simp only [Real.norm_natCast, Nat.cast_one, norm_one, mul_one] at hb + have hqc : q x.length ≤ ⌈C⌉₊ := by exact_mod_cast le_trans hb (Nat.le_ceil C) + exact le_trans hqc (le_max_left _ _) + +/-- The witness block starting at `start` and holding `len` answers. -/ +def wBlock (w : List Bool) (start len : ℕ) : List Bool := (w.drop start).take len + +theorem wBlock_mem_FP {w s l : List Bool → List Bool} + (hw : w ∈ FP) (hs : s ∈ FP) (hl : l ∈ FP) : + (fun z => wBlock (w z) (s z).length (l z).length) ∈ FP := + Cobham.takeLenFn_mem_FP hl (dropLenFn_mem_FP hs hw) + +theorem length_wBlock {w : List Bool} {start len : ℕ} (h : start + len ≤ w.length) : + (wBlock w start len).length = len := by + rw [wBlock, List.length_take, List.length_drop] + omega + +theorem getElem_wBlock {w : List Bool} {start len : ℕ} (h : start + len ≤ w.length) + {i : ℕ} (hi : i < len) : + (wBlock w start len)[i]'(by rw [length_wBlock h]; exact hi) = w.getD (start + i) false := by + have hlt : start + i < w.length := by omega + simp only [wBlock] + rw [List.getElem_take, List.getElem_drop] + rw [List.getD_eq_getElem?_getD, List.getElem?_eq_getElem hlt] + rfl + +/-- **The block is the answer list.** With the answers for coin index `c` laid +out in the slots `c * Q, …`, the witness block is exactly the table +`SubsetNP.tableOf` reads. -/ +theorem tableOf_eq_wBlock (V : PCPVerifier) (t Q : ℕ) (x w : List Bool) + (ρ : Fin t → Bool) + (h : PCPVerifier.coinIndex ρ * Q + (V.positions x (BitString.toList ρ)).length + ≤ w.length) : + V.tableOf t Q x w ρ + = wBlock w (PCPVerifier.coinIndex ρ * Q) + (V.positions x (BitString.toList ρ)).length := by + refine List.ext_getElem ?_ fun i h1 h2 => ?_ + · rw [V.length_tableOf t Q x w ρ, length_wBlock h] + · have hi : i < (V.positions x (BitString.toList ρ)).length := by + rwa [V.length_tableOf t Q x w ρ] at h1 + rw [getElem_wBlock h hi] + show (List.map _ (List.range _))[i] = _ + rw [List.getElem_map, List.getElem_range] + +/-- A one-bit block is the bit it holds. -/ +theorem wBlock_one {w : List Bool} {k : ℕ} (h : k < w.length) : + wBlock w k 1 = [w.getD k false] := by + rw [wBlock, List.drop_eq_getElem_cons h] + simp only [List.take_succ_cons, List.take_zero] + congr 1 + rw [List.getD_eq_getElem?_getD, List.getElem?_eq_getElem h] + rfl + +/-- Reading a slot of the table. -/ +theorem getElem?_tableOf (V : PCPVerifier) (t Q : ℕ) (x w : List Bool) + (ρ : Fin t → Bool) {i : ℕ} (hi : i < (V.positions x (BitString.toList ρ)).length) : + (V.tableOf t Q x w ρ)[i]? = some (w.getD (PCPVerifier.coinIndex ρ * Q + i) false) := by + rw [PCPVerifier.tableOf, List.getElem?_map, List.getElem?_range hi] + rfl + +/-! ### Acceptance on every coin string + +The check is a loop over coin indices. Each iteration recovers the coin string +from its index, reads the verifier's query list to learn how many answers this +coin string uses, cuts that many out of the witness block, and asks the verdict. +-/ + +section Acceptance + +variable (V : PCPVerifier) (f : List Bool → List Bool) (r : ℕ → ℕ) (Q : ℕ) + +/-- The input of one iteration is `pair (pair x w) (unary c)`. -/ +def accX (y : List Bool) : List Bool := pairFst (pairFst y) + +/-- The witness, out of the iteration's input. -/ +def accW (y : List Bool) : List Bool := pairSnd (pairFst y) + +/-- The coin string named by the iteration's index. -/ +noncomputable def accCoin (y : List Bool) : List Bool := + coinStr (r (accX y).length) (pairSnd y).length + +/-- The verifier's view: input and coins paired with the answers read off the +witness block. -/ +noncomputable def accView (y : List Bool) : List Bool := + pair (pair (accX y) (accCoin r y)) + (wBlock (accW y) ((pairSnd y).length * Q) + (posCount (f (pair (accX y) (accCoin r y)))).length) + +theorem accX_mem_FP : accX ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.fstBlock_mem_FP + +theorem accW_mem_FP : accW ∈ FP := + mem_FP_comp Cobham.fstBlock_mem_FP Cobham.sndBlock_mem_FP + +theorem accCoin_mem_FP + (hr : (fun x : List Bool => List.replicate (r x.length) true) ∈ FP) : + accCoin r ∈ FP := by + have ht : (fun y : List Bool => List.replicate (r (accX y).length) true) ∈ FP := by + have := mem_FP_comp accX_mem_FP hr + simpa using this + have hc : (fun y : List Bool => List.replicate (pairSnd y).length true) ∈ FP := by + have := mem_FP_comp Cobham.sndBlock_mem_FP unaryLength_mem_FP + simpa using this + exact coinStr_mem_FP ht hc + +theorem accView_mem_FP (hf : f ∈ FP) + (hr : (fun x : List Bool => List.replicate (r x.length) true) ∈ FP) : + accView f r Q ∈ FP := by + have hcoin := accCoin_mem_FP r hr + have hview : (fun y => pair (accX y) (accCoin r y)) ∈ FP := + Cobham.pairFn_mem_FP accX_mem_FP hcoin + have hfv : (fun y => f (pair (accX y) (accCoin r y))) ∈ FP := by + have := mem_FP_comp hview hf + simpa using this + have hcount : (fun y => posCount (f (pair (accX y) (accCoin r y)))) ∈ FP := + posCount_mem_FP hfv + have hoff : (fun y : List Bool => + List.replicate ((pairSnd y).length * Q) false) ∈ FP := by + have hb : (fun _ : List Bool => List.replicate Q false) ∈ FP := + Cobham.const_replicate_mem_FP Q + have := Cobham.mulLenFn_mem_FP Cobham.sndBlock_mem_FP hb + refine mem_FP_of_eq this fun y => ?_ + rw [List.length_replicate] + have hblk : (fun y => wBlock (accW y) + (List.replicate ((pairSnd y).length * Q) false).length + (posCount (f (pair (accX y) (accCoin r y)))).length) ∈ FP := + wBlock_mem_FP accW_mem_FP hoff hcount + refine Cobham.pairFn_mem_FP hview (mem_FP_of_eq hblk fun y => ?_) + rw [List.length_replicate] + +/-- One iteration's condition: the verifier accepts the view. -/ +noncomputable def accInner : Language := accView f r Q ⁻¹' V.verdict + +theorem accInner_mem_P (hf : f ∈ FP) + (hr : (fun x : List Bool => List.replicate (r x.length) true) ∈ FP) : + accInner V f r Q ∈ P := + mem_P_preimage (accView_mem_FP f r Q hf hr) V.verdict_mem + +/-- **Acceptance on every coin string**, as a language of `pair x w`. -/ +noncomputable def accLang : Language := + {z : List Bool | ∀ c < 2 ^ r (pairFst z).length, + pair z (List.replicate c true) ∈ accInner V f r Q} + +open scoped Complexity in +theorem accLang_mem_P (hf : f ∈ FP) + (hr : (fun x : List Bool => List.replicate (r x.length) true) ∈ FP) + (hrlog : r =O fun n => Nat.log 2 n) : + accLang V f r Q ∈ P := by + have hlen : (fun z : List Bool => + List.replicate (2 ^ r (pairFst z).length) true) ∈ FP := by + have := mem_FP_comp Cobham.fstBlock_mem_FP (unaryExp_mem_FP_of_bigO_log hr hrlog) + simpa using this + exact forall_unary_mem_P (accInner_mem_P V f r Q hf hr) hlen + +/-- What one iteration looks at, on a well-formed input. -/ +theorem accView_pair + (hfspec : ∀ x rr : List Bool, + f (pair x rr) = DataEncode.bitstringEncode (V.positions x rr)) + {x w : List Bool} {c : ℕ} (hc : c < 2 ^ r x.length) + (hw : w.length = 2 ^ r x.length * Q) + (hQ : ∀ rr : List Bool, (V.positions x rr).length ≤ Q) : + accView f r Q (pair (pair x w) (List.replicate c true)) + = pair (pair x (BitString.toList (PCPVerifier.coinOfIndex (t := r x.length) ⟨c, hc⟩))) + (V.tableOf (r x.length) Q x w + (PCPVerifier.coinOfIndex (t := r x.length) ⟨c, hc⟩)) := by + set ρ := PCPVerifier.coinOfIndex (t := r x.length) ⟨c, hc⟩ with hρ + have hX : accX (pair (pair x w) (List.replicate c true)) = x := by + rw [accX, pairFst_pair, pairFst_pair] + have hW : accW (pair (pair x w) (List.replicate c true)) = w := by + rw [accW, pairFst_pair, pairSnd_pair] + have hC : (pairSnd (pair (pair x w) (List.replicate c true))).length = c := by + rw [pairSnd_pair, List.length_replicate] + have hcoin : accCoin r (pair (pair x w) (List.replicate c true)) = BitString.toList ρ := by + rw [accCoin, hX, hC, coinStr_eq hc, hρ, toList_coinOfIndex] + have hidx : PCPVerifier.coinIndex ρ = c := coinIndex_coinOfIndex _ + have hlen : (posCount (f (pair x (BitString.toList ρ)))).length + = (V.positions x (BitString.toList ρ)).length := by + rw [hfspec, posCount_eq, List.length_replicate] + have hfit : PCPVerifier.coinIndex ρ * Q + + (V.positions x (BitString.toList ρ)).length ≤ w.length := by + rw [hidx, hw] + have h1 : c + 1 ≤ 2 ^ r x.length := hc + have h2 : (V.positions x (BitString.toList ρ)).length ≤ Q := hQ _ + calc c * Q + (V.positions x (BitString.toList ρ)).length + ≤ c * Q + Q := by omega + _ = (c + 1) * Q := by ring + _ ≤ 2 ^ r x.length * Q := Nat.mul_le_mul_right _ h1 + rw [accView, hX, hW, hC, hcoin, hlen, ← hidx, ← tableOf_eq_wBlock V _ _ _ _ _ hfit] + +/-- **Acceptance on every coin string.** -/ +theorem mem_accLang_iff + (hfspec : ∀ x rr : List Bool, + f (pair x rr) = DataEncode.bitstringEncode (V.positions x rr)) + {x w : List Bool} (hw : w.length = 2 ^ r x.length * Q) + (hQ : ∀ rr : List Bool, (V.positions x rr).length ≤ Q) : + pair x w ∈ accLang V f r Q + ↔ ∀ ρ : Fin (r x.length) → Bool, + pair (pair x (BitString.toList ρ)) (V.tableOf (r x.length) Q x w ρ) ∈ V.verdict := by + have hfst : pairFst (pair x w) = x := pairFst_pair x w + constructor + · intro h ρ + have hc : PCPVerifier.coinIndex ρ < 2 ^ r x.length := PCPVerifier.coinIndex_lt ρ + have := h (PCPVerifier.coinIndex ρ) (by rwa [hfst]) + rw [accInner, Set.mem_preimage, accView_pair V f r Q hfspec hc hw hQ, + PCPVerifier.coinOfIndex_coinIndex ρ hc] at this + exact this + · intro h c hc + rw [hfst] at hc + rw [accInner, Set.mem_preimage, accView_pair V f r Q hfspec hc hw hQ] + exact h _ + +end Acceptance + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/Walk.lean b/Complexitylib/Classes/PCP/Internal/Walk.lean new file mode 100644 index 00000000..89633fc4 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/Walk.lean @@ -0,0 +1,95 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.Mixing +public import Mathlib.Data.Fin.Tuple.Basic + +/-! +# Walks in a regular graph + +Dinur's powering step replaces the edges of a constraint graph by its **walks** +of a fixed length `t`. This module gives walks their combinatorial form — a +starting vertex together with a tuple of `t` edge labels — and connects that +form to the analytic one: summing any function of the walk's endpoint over all +`deg ^ t` walks out of a vertex is `deg ^ t` times the `t`-step walk operator. + +That identity, `sum_walkEnd`, is the bridge between the two views. The powering +construction is defined by quantifying over walk tuples, while every estimate +about it comes from the spectral bound through `Mixing`; `sum_mul_walkEnd` +performs the translation in the form the analysis needs. + +## Main definitions + +- `RegGraph.walkEnd` — the endpoint of the walk from `v` with label tuple `s` + +## Main results + +- `RegGraph.sum_walkEnd` — `∑ s, f (walkEnd v s) = d ^ t * stepIter t f v` +- `RegGraph.card_walks` — there are `deg ^ t` walks out of each vertex +-/ + +@[expose] public section + +namespace Complexity + +namespace RegGraph + +variable (G : RegGraph) + +/-- The endpoint of the walk that starts at `v` and follows the edge labels +`s 0, s 1, …, s (t-1)` in order. -/ +def walkEnd (G : RegGraph) : ∀ (t : ℕ), G.V → (Fin t → G.D) → G.V + | 0, v, _ => v + | t + 1, v, s => walkEnd G t (G.nbr v (s 0)) (fun j => s j.succ) + +@[simp] theorem walkEnd_zero (v : G.V) (s : Fin 0 → G.D) : G.walkEnd 0 v s = v := by + simp [walkEnd] + +theorem walkEnd_succ (t : ℕ) (v : G.V) (s : Fin (t + 1) → G.D) : + G.walkEnd (t + 1) v s = G.walkEnd t (G.nbr v (s 0)) (fun j => s j.succ) := by + simp [walkEnd] + +theorem walkEnd_cons (t : ℕ) (v : G.V) (i : G.D) (s : Fin t → G.D) : + G.walkEnd (t + 1) v (Fin.cons i s) = G.walkEnd t (G.nbr v i) s := by + rw [walkEnd_succ] + simp + +/-- There are `deg ^ t` walks of length `t` out of a vertex. -/ +theorem card_walks (t : ℕ) : Fintype.card (Fin t → G.D) = G.deg ^ t := by + simp + +/-- **The bridge between walks and the walk operator.** Averaging a function of +the endpoint over all walks of length `t` out of `v` is exactly the `t`-step +operator applied at `v`. -/ +theorem sum_walkEnd (f : G.V → ℝ) (t : ℕ) (v : G.V) : + ∑ s : Fin t → G.D, f (G.walkEnd t v s) = (G.deg : ℝ) ^ t * G.stepIter t f v := by + induction t generalizing v with + | zero => simp + | succ t ih => + have hd : (G.deg : ℝ) ≠ 0 := G.deg_ne_zero + have hsplit : ∑ s : Fin (t + 1) → G.D, f (G.walkEnd (t + 1) v s) + = ∑ p : G.D × (Fin t → G.D), f (G.walkEnd (t + 1) v (Fin.cons p.1 p.2)) := + (Equiv.sum_comp (Fin.consEquiv fun _ => G.D) + (fun s => f (G.walkEnd (t + 1) v s))).symm + calc ∑ s : Fin (t + 1) → G.D, f (G.walkEnd (t + 1) v s) + = ∑ p : G.D × (Fin t → G.D), f (G.walkEnd (t + 1) v (Fin.cons p.1 p.2)) := + hsplit + _ = ∑ i : G.D, ∑ s : Fin t → G.D, f (G.walkEnd t (G.nbr v i) s) := by + rw [Fintype.sum_prod_type] + exact Finset.sum_congr rfl fun i _ => + Finset.sum_congr rfl fun s _ => by rw [walkEnd_cons] + _ = ∑ i : G.D, (G.deg : ℝ) ^ t * G.stepIter t f (G.nbr v i) := by + exact Finset.sum_congr rfl fun i _ => ih (G.nbr v i) + _ = (G.deg : ℝ) ^ t * ∑ i : G.D, G.stepIter t f (G.nbr v i) := by + rw [Finset.mul_sum] + _ = (G.deg : ℝ) ^ (t + 1) * G.stepIter (t + 1) f v := by + rw [stepIter_succ, step] + field_simp + ring + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/WalkDart.lean b/Complexitylib/Classes/PCP/Internal/WalkDart.lean new file mode 100644 index 00000000..e044bfca --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/WalkDart.lean @@ -0,0 +1,243 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.WalkPath + +/-! +# Each step of a random walk is a uniform dart + +The counting fact behind every first-moment estimate in Dinur's powering +analysis: fix a step index `k < t`; then the map sending a walk `(v, s)` to its +`k`-th dart `(walkAt k, s k)` sends the uniform distribution on walks to the +uniform distribution on darts. Equivalently, summing any function of the `k`-th +dart over all `order · deg ^ t` walks gives `deg ^ (t-1)` times its sum over all +darts — every dart is the `k`-th dart of exactly `deg ^ (t-1)` walks. + +The proof is an induction on `k` that peels the first label off the walk. Two +ingredients do the work: `walkAt_cons`, which says dropping the first label +shifts the trajectory by one, and `sum_nbr_nsmul`, the rotation-map form of +regularity, which reindexes the sum over `(v, i)` as `deg` copies of the sum +over vertices. + +Stated for an arbitrary `AddCommMonoid`, since it is used both to count walks in +`ℕ` and to compute real-valued averages. + +## Main results + +- `RegGraph.walkAt_cons` — dropping the first label shifts the trajectory +- `RegGraph.sum_stepDart_fixed` — from a fixed start, the `k`-th dart is + described by the `k`-step walk operator +-/ + +@[expose] public section + +namespace Complexity + +namespace RegGraph + +variable (G : RegGraph) + +/-- Dropping the first label of a walk shifts its trajectory by one step. -/ +theorem walkAt_cons {t : ℕ} (v : G.V) (i : G.D) (s : Fin t → G.D) : + ∀ m : ℕ, G.walkAt (t + 1) v (Fin.cons i s) (m + 1) = G.walkAt t (G.nbr v i) s m := by + intro m + induction m with + | zero => + rw [G.walkAt_succ_of_lt _ _ (Nat.succ_pos t), walkAt_zero, walkAt_zero] + congr 1 + | succ m ih => + rcases Nat.lt_or_ge m t with hlt | hge + · rw [G.walkAt_succ_of_lt _ _ (by omega : m + 1 < t + 1), ih, + G.walkAt_succ_of_lt _ _ hlt] + congr 1 + · rw [G.walkAt_succ_of_ge _ _ (by omega : t + 1 ≤ m + 1), ih, + G.walkAt_succ_of_ge _ _ hge] + +/-- Summing over label tuples splits into the first label and the rest. -/ +theorem sum_cons_split {M : Type*} [AddCommMonoid M] (m : ℕ) (F : (Fin (m + 1) → G.D) → M) : + (∑ r : Fin (m + 1) → G.D, F r) + = ∑ i : G.D, ∑ r : Fin m → G.D, F (Fin.cons i r) := by + rw [← Equiv.sum_comp (Fin.consEquiv fun _ => G.D) F] + exact Fintype.sum_prod_type (fun p : G.D × (Fin m → G.D) => F (Fin.cons p.1 p.2)) + +/-- **The `k`-th dart of a walk out of a *fixed* start.** Unlike `sum_stepDart`, +where the start is also averaged and the dart comes out uniform, here the dart's +vertex is distributed as the `k`-step walk from `x`, which the walk operator +describes exactly. This is what turns a correlation between two steps of a walk +into an operator inner product, where `Mixing` can bound it. -/ +theorem sum_stepDart_fixed (h : G.V → G.D → ℝ) : + ∀ (k m : ℕ) (hk : k < m) (x : G.V), + (∑ r : Fin m → G.D, h (G.walkAt m x r k) (r ⟨k, hk⟩)) + = (G.deg : ℝ) ^ (m - 1) * ∑ a : G.D, G.stepIter k (fun y => h y a) x := by + intro k + induction k with + | zero => + intro m hk x + obtain ⟨m', rfl⟩ : ∃ m', m = m' + 1 := ⟨m - 1, by omega⟩ + have hsplit : (∑ r : Fin (m' + 1) → G.D, h (G.walkAt (m' + 1) x r 0) (r ⟨0, hk⟩)) + = ∑ a : G.D, ∑ _r : Fin m' → G.D, h x a := by + rw [G.sum_cons_split m' (fun r => h (G.walkAt (m' + 1) x r 0) (r ⟨0, hk⟩))] + refine Finset.sum_congr rfl fun a _ => ?_ + refine Finset.sum_congr rfl fun r _ => ?_ + have h0 : (⟨0, hk⟩ : Fin (m' + 1)) = 0 := rfl + rw [h0, Fin.cons_zero, walkAt_zero] + rw [hsplit] + have hcard : ∀ a : G.D, (∑ _r : Fin m' → G.D, h x a) = (G.deg : ℝ) ^ m' * h x a := by + intro a + rw [Finset.sum_const, Finset.card_univ, G.card_walks m', nsmul_eq_mul] + push_cast + ring + rw [Finset.sum_congr rfl fun a _ => hcard a, ← Finset.mul_sum] + simp + | succ k ih => + intro m hk x + obtain ⟨m', rfl⟩ : ∃ m', m = m' + 1 := ⟨m - 1, by omega⟩ + have hkm : k < m' := by omega + have hd : (G.deg : ℝ) ≠ 0 := G.deg_ne_zero + have hsplit : (∑ r : Fin (m' + 1) → G.D, + h (G.walkAt (m' + 1) x r (k + 1)) (r ⟨k + 1, hk⟩)) + = ∑ i : G.D, ∑ r : Fin m' → G.D, + h (G.walkAt m' (G.nbr x i) r k) (r ⟨k, hkm⟩) := by + rw [G.sum_cons_split m' (fun r => h (G.walkAt (m' + 1) x r (k + 1)) (r ⟨k + 1, hk⟩))] + refine Finset.sum_congr rfl fun i _ => ?_ + refine Finset.sum_congr rfl fun r _ => ?_ + have hidx : (Fin.cons (α := fun _ => G.D) i r) ⟨k + 1, hk⟩ = r ⟨k, hkm⟩ := by + rw [show (⟨k + 1, hk⟩ : Fin (m' + 1)) = Fin.succ ⟨k, hkm⟩ from rfl, Fin.cons_succ] + rw [hidx, G.walkAt_cons x i r k] + rw [hsplit] + have hstep : ∀ i : G.D, (∑ r : Fin m' → G.D, + h (G.walkAt m' (G.nbr x i) r k) (r ⟨k, hkm⟩)) + = (G.deg : ℝ) ^ (m' - 1) * ∑ a : G.D, G.stepIter k (fun y => h y a) (G.nbr x i) := + fun i => ih m' hkm (G.nbr x i) + rw [Finset.sum_congr rfl fun i _ => hstep i, ← Finset.mul_sum] + have hswap : (∑ i : G.D, ∑ a : G.D, G.stepIter k (fun y => h y a) (G.nbr x i)) + = ∑ a : G.D, ∑ i : G.D, G.stepIter k (fun y => h y a) (G.nbr x i) := + Finset.sum_comm + rw [hswap] + have hnbr : ∀ a : G.D, (∑ i : G.D, G.stepIter k (fun y => h y a) (G.nbr x i)) + = (G.deg : ℝ) * G.stepIter (k + 1) (fun y => h y a) x := by + intro a + rw [stepIter_succ, step] + field_simp + rw [Finset.sum_congr rfl fun a _ => hnbr a, ← Finset.mul_sum, ← mul_assoc] + congr 1 + rw [← pow_succ] + congr 1 + omega + +/-- **Two darts of a walk out of a fixed start.** The correlation between what +happens at step `k` and at step `l > k` is an operator expression: the walk +reaches step `k`, the constraint there is weighted, and the remaining `l-k-1` +steps are another application of the walk operator. Feeding this to `Mixing` is +how the second moment of the number of faulty steps gets bounded. -/ +theorem sum_two_darts_fixed (h₁ h₂ : G.V → G.D → ℝ) : + ∀ (k l m : ℕ) (hkl : k < l) (hl : l < m) (x : G.V), + (∑ r : Fin m → G.D, h₁ (G.walkAt m x r k) (r ⟨k, by omega⟩) + * h₂ (G.walkAt m x r l) (r ⟨l, by omega⟩)) + = (G.deg : ℝ) ^ (m - 2) * ∑ a : G.D, ∑ b : G.D, + G.stepIter k + (fun y => h₁ y a * G.stepIter (l - k - 1) (fun z => h₂ z b) (G.nbr y a)) x := by + intro k + induction k with + | zero => + intro l m hkl hl x + obtain ⟨m', rfl⟩ : ∃ m', m = m' + 1 := ⟨m - 1, by omega⟩ + obtain ⟨l', rfl⟩ : ∃ l', l = l' + 1 := ⟨l - 1, by omega⟩ + have hl' : l' < m' := by omega + have hsplit : (∑ r : Fin (m' + 1) → G.D, + h₁ (G.walkAt (m' + 1) x r 0) (r ⟨0, by omega⟩) + * h₂ (G.walkAt (m' + 1) x r (l' + 1)) (r ⟨l' + 1, by omega⟩)) + = ∑ a : G.D, ∑ r : Fin m' → G.D, + h₁ x a * h₂ (G.walkAt m' (G.nbr x a) r l') (r ⟨l', hl'⟩) := by + rw [G.sum_cons_split m' (fun r => h₁ (G.walkAt (m' + 1) x r 0) (r ⟨0, by omega⟩) + * h₂ (G.walkAt (m' + 1) x r (l' + 1)) (r ⟨l' + 1, by omega⟩))] + refine Finset.sum_congr rfl fun a _ => ?_ + refine Finset.sum_congr rfl fun r _ => ?_ + have h0 : (⟨0, by omega⟩ : Fin (m' + 1)) = 0 := rfl + have hidx : (Fin.cons (α := fun _ => G.D) a r) ⟨l' + 1, by omega⟩ = r ⟨l', hl'⟩ := by + rw [show (⟨l' + 1, by omega⟩ : Fin (m' + 1)) = Fin.succ ⟨l', hl'⟩ from rfl, + Fin.cons_succ] + rw [h0, Fin.cons_zero, walkAt_zero, hidx, G.walkAt_cons x a r l'] + rw [hsplit] + have hinner : ∀ a : G.D, (∑ r : Fin m' → G.D, + h₁ x a * h₂ (G.walkAt m' (G.nbr x a) r l') (r ⟨l', hl'⟩)) + = h₁ x a * ((G.deg : ℝ) ^ (m' - 1) + * ∑ b : G.D, G.stepIter l' (fun z => h₂ z b) (G.nbr x a)) := by + intro a + rw [← Finset.mul_sum, G.sum_stepDart_fixed h₂ l' m' hl' (G.nbr x a)] + rw [Finset.sum_congr rfl fun a _ => hinner a] + have hpow : m' + 1 - 2 = m' - 1 := by omega + rw [hpow, Finset.mul_sum] + refine Finset.sum_congr rfl fun a _ => ?_ + have hsub : l' + 1 - 0 - 1 = l' := by omega + simp only [stepIter_zero, hsub, ← Finset.mul_sum] + ring + | succ k ih => + intro l m hkl hl x + obtain ⟨m', rfl⟩ : ∃ m', m = m' + 1 := ⟨m - 1, by omega⟩ + obtain ⟨l', rfl⟩ : ∃ l', l = l' + 1 := ⟨l - 1, by omega⟩ + have hkl' : k < l' := by omega + have hl' : l' < m' := by omega + have hd : (G.deg : ℝ) ≠ 0 := G.deg_ne_zero + have hsplit : (∑ r : Fin (m' + 1) → G.D, + h₁ (G.walkAt (m' + 1) x r (k + 1)) (r ⟨k + 1, by omega⟩) + * h₂ (G.walkAt (m' + 1) x r (l' + 1)) (r ⟨l' + 1, by omega⟩)) + = ∑ i : G.D, ∑ r : Fin m' → G.D, + h₁ (G.walkAt m' (G.nbr x i) r k) (r ⟨k, by omega⟩) + * h₂ (G.walkAt m' (G.nbr x i) r l') (r ⟨l', hl'⟩) := by + rw [G.sum_cons_split m' (fun r => h₁ (G.walkAt (m' + 1) x r (k + 1)) (r ⟨k + 1, by omega⟩) + * h₂ (G.walkAt (m' + 1) x r (l' + 1)) (r ⟨l' + 1, by omega⟩))] + refine Finset.sum_congr rfl fun i _ => ?_ + refine Finset.sum_congr rfl fun r _ => ?_ + have hk1 : (Fin.cons (α := fun _ => G.D) i r) ⟨k + 1, by omega⟩ = r ⟨k, by omega⟩ := by + rw [show (⟨k + 1, by omega⟩ : Fin (m' + 1)) = Fin.succ ⟨k, by omega⟩ from rfl, + Fin.cons_succ] + have hl1 : (Fin.cons (α := fun _ => G.D) i r) ⟨l' + 1, by omega⟩ = r ⟨l', hl'⟩ := by + rw [show (⟨l' + 1, by omega⟩ : Fin (m' + 1)) = Fin.succ ⟨l', hl'⟩ from rfl, + Fin.cons_succ] + rw [hk1, hl1, G.walkAt_cons x i r k, G.walkAt_cons x i r l'] + rw [hsplit] + have hIH : ∀ i : G.D, (∑ r : Fin m' → G.D, + h₁ (G.walkAt m' (G.nbr x i) r k) (r ⟨k, by omega⟩) + * h₂ (G.walkAt m' (G.nbr x i) r l') (r ⟨l', hl'⟩)) + = (G.deg : ℝ) ^ (m' - 2) * ∑ a : G.D, ∑ b : G.D, + G.stepIter k + (fun y => h₁ y a * G.stepIter (l' - k - 1) (fun z => h₂ z b) (G.nbr y a)) + (G.nbr x i) := + fun i => ih l' m' hkl' hl' (G.nbr x i) + rw [Finset.sum_congr rfl fun i _ => hIH i, ← Finset.mul_sum] + have hcomm : (∑ i : G.D, ∑ a : G.D, ∑ b : G.D, + G.stepIter k (fun y => h₁ y a + * G.stepIter (l' - k - 1) (fun z => h₂ z b) (G.nbr y a)) (G.nbr x i)) + = ∑ a : G.D, ∑ b : G.D, ∑ i : G.D, + G.stepIter k (fun y => h₁ y a + * G.stepIter (l' - k - 1) (fun z => h₂ z b) (G.nbr y a)) (G.nbr x i) := by + rw [Finset.sum_comm] + refine Finset.sum_congr rfl fun a _ => ?_ + rw [Finset.sum_comm] + rw [hcomm] + have hstep : ∀ a b : G.D, (∑ i : G.D, + G.stepIter k (fun y => h₁ y a + * G.stepIter (l' - k - 1) (fun z => h₂ z b) (G.nbr y a)) (G.nbr x i)) + = (G.deg : ℝ) * G.stepIter (k + 1) (fun y => h₁ y a + * G.stepIter (l' - k - 1) (fun z => h₂ z b) (G.nbr y a)) x := by + intro a b + rw [stepIter_succ, step] + field_simp + rw [Finset.sum_congr rfl fun a _ => Finset.sum_congr rfl fun b _ => hstep a b] + have hsub : l' + 1 - (k + 1) - 1 = l' - k - 1 := by omega + simp only [hsub] + have hpow : (G.deg : ℝ) ^ (m' + 1 - 2) = (G.deg : ℝ) ^ (m' - 2) * (G.deg : ℝ) := by + rw [← pow_succ] + congr 1 + omega + rw [hpow] + simp only [← Finset.mul_sum] + ring + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/WalkPath.lean b/Complexitylib/Classes/PCP/Internal/WalkPath.lean new file mode 100644 index 00000000..0338dc97 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/WalkPath.lean @@ -0,0 +1,143 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.Walk + +/-! +# Trajectories of walks, and self-loops + +`Walk` gives the *endpoint* of a walk, which is all the spectral estimates +need. Dinur's powering step also needs the walk's whole **trajectory**: the +constraint attached to a length-`t` walk talks about the graph constraints on +each intermediate edge, and about what the two endpoints of the walk claim the +intermediate vertices are labelled. + +This module adds the trajectory `walkAt`, indexed by `ℕ` and constant once the +walk is exhausted, and identifies its final position with `walkEnd`. The bridge +runs through `walkEnd_snoc`: extending a walk by one label at the *end* takes +one more step from its endpoint, whereas `walkEnd` recurses on the *first* +label. + +It also introduces `Loops`, a choice of self-loop at each vertex. Dinur's +construction needs these to pad a short walk out to a fixed length without +moving: a vertex within distance `k ≤ h` of `v` is the endpoint of a length-`h` +walk from `v` that follows `k` real steps and then stays put. + +## Main definitions + +- `RegGraph.walkAt` — the position of the walk after `k` steps (`k > t` stays) +- `RegGraph.Loops` — a self-loop at every vertex + +## Main results + +- `RegGraph.walkEnd_snoc` — appending a label takes one further step +- `RegGraph.walkAt_eq_walkEnd_prefix`, `RegGraph.walkAt_self_eq_walkEnd` +- `RegGraph.Loops.nbr_loop` — a loop label does not move +-/ + +@[expose] public section + +namespace Complexity + +namespace RegGraph + +variable (G : RegGraph) + +/-! ### Extending a walk at its end -/ + +theorem walkEnd_snoc : ∀ (t : ℕ) (v : G.V) (s : Fin t → G.D) (i : G.D), + G.walkEnd (t + 1) v (Fin.snoc s i) = G.nbr (G.walkEnd t v s) i := by + intro t + induction t with + | zero => + intro v s i + have h0 : (0 : Fin 1) = Fin.last 0 := rfl + rw [walkEnd_succ, h0, Fin.snoc_last] + simp + | succ t ih => + intro v s i + have hcs : (Fin.snoc s i : Fin (t + 2) → G.D) + = Fin.cons (s 0) (Fin.snoc (α := fun _ => G.D) (fun j => s j.succ) i) := by + rw [Fin.cons_snoc_eq_snoc_cons] + congr 1 + exact (Fin.cons_self_tail s).symm + rw [hcs, walkEnd_cons, ih, walkEnd_succ] + +/-! ### The trajectory -/ + +/-- The position of the walk `(v, s)` after `k` steps. Once the label tuple is +exhausted the walk stays where it is, so this is defined for every `k : ℕ`. -/ +def walkAt (G : RegGraph) (t : ℕ) (v : G.V) (s : Fin t → G.D) : ℕ → G.V + | 0 => v + | k + 1 => if h : k < t then G.nbr (walkAt G t v s k) (s ⟨k, h⟩) else walkAt G t v s k + +@[simp] theorem walkAt_zero (t : ℕ) (v : G.V) (s : Fin t → G.D) : + G.walkAt t v s 0 = v := rfl + +theorem walkAt_succ_of_lt {t : ℕ} (v : G.V) (s : Fin t → G.D) {k : ℕ} (h : k < t) : + G.walkAt t v s (k + 1) = G.nbr (G.walkAt t v s k) (s ⟨k, h⟩) := by + rw [walkAt] + simp [h] + +theorem walkAt_succ_of_ge {t : ℕ} (v : G.V) (s : Fin t → G.D) {k : ℕ} (h : t ≤ k) : + G.walkAt t v s (k + 1) = G.walkAt t v s k := by + rw [walkAt] + simp [Nat.not_lt.mpr h] + +/-- The trajectory after `k ≤ t` steps is the endpoint of the length-`k` prefix +of the walk. -/ +theorem walkAt_eq_walkEnd_prefix {t : ℕ} (v : G.V) (s : Fin t → G.D) : + ∀ (k : ℕ) (hk : k ≤ t), + G.walkAt t v s k = G.walkEnd k v (fun j : Fin k => s (Fin.castLE hk j)) := by + intro k + induction k with + | zero => intro _; simp + | succ k ih => + intro hk + have hkt : k < t := by omega + have hk' : k ≤ t := le_of_lt hkt + have hsnoc : (fun j : Fin (k + 1) => s (Fin.castLE hk j)) + = Fin.snoc (α := fun _ => G.D) + (fun j : Fin k => s (Fin.castLE hk' j)) (s ⟨k, hkt⟩) := by + funext j + refine Fin.lastCases ?_ ?_ j + · rw [Fin.snoc_last] + congr 1 + · intro j' + rw [Fin.snoc_castSucc] + congr 1 + rw [G.walkAt_succ_of_lt v s hkt, ih hk', hsnoc, G.walkEnd_snoc] + +/-- The trajectory ends where `walkEnd` says it does. -/ +theorem walkAt_self_eq_walkEnd {t : ℕ} (v : G.V) (s : Fin t → G.D) : + G.walkAt t v s t = G.walkEnd t v s := by + rw [G.walkAt_eq_walkEnd_prefix v s t le_rfl] + congr 1 + +/-! ### Self-loops -/ + +/-- A choice of self-loop at every vertex: an edge label that fixes the dart, +hence does not move. Dinur's construction needs these to pad short walks out to +a fixed length. -/ +structure Loops (G : RegGraph) where + /-- The self-loop label at each vertex. -/ + loop : G.V → G.D + /-- The chosen dart is fixed by reversal, so it is a self-loop. -/ + rot_loop : ∀ v, G.rot (v, loop v) = (v, loop v) + +namespace Loops + +variable {G} (L : G.Loops) + +/-- Following a loop label does not move. -/ +@[simp] theorem nbr_loop (v : G.V) : G.nbr v (L.loop v) = v := by + rw [nbr, L.rot_loop v] + +end Loops + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/WalkSplit.lean b/Complexitylib/Classes/PCP/Internal/WalkSplit.lean new file mode 100644 index 00000000..7a1512b9 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/WalkSplit.lean @@ -0,0 +1,324 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.KilledWalk + +/-! +# Splitting a walk at a step + +The surgery behind the crossing decomposition: a walk of length `ℓ` and a +position `i < ℓ` split it into a prefix of length `i`, the label crossed at `i`, +and a suffix of length `ℓ - (i+1)`; gluing puts them back. + +Dinur's first-moment count is organised around this. Conditioned on a killed +walk crossing a given dart at step `i`, what remains is exactly a free prefix +and a free suffix — and because the killed law makes their *lengths* +independent too, the count factorises into two copies of the plurality bound. + +## Main definitions + +- `RegGraph.segPre`, `RegGraph.segSuf` — the two pieces of a split walk +- `RegGraph.segGlue` — gluing them back with a crossing label + +## Main results + +- `RegGraph.segPre_segGlue`, `RegGraph.segMid_segGlue`, `RegGraph.segSuf_segGlue` + — gluing then splitting is the identity +- `RegGraph.segGlue_split` — splitting then gluing is the identity +- `RegGraph.walkAt_segSuf`, `RegGraph.revWalk_segSuf` — the suffix walk tracks + the original, and reversing it gives the reversed walk's prefix +- `RegGraph.card_crossing_eq` — the walks crossing a dart at a step, counted as + independent prefix and suffix factors +- `RegGraph.card_label_fibre` — labels grouped by their effective walk +- `RegGraph.card_label_crossing` — the two combined: crossing labels counted as + prefix times suffix times fibre weight +- `RegGraph.walkAt_preWalk` — a killed walk's crossings are the underlying + fixed-length walk's crossings +-/ + +@[expose] public section + +namespace Complexity + +namespace RegGraph + +variable (G : RegGraph) + +/-- The first `i` steps of a walk. -/ +def segPre {ℓ : ℕ} (W : Fin ℓ → G.D) {i : ℕ} (hi : i ≤ ℓ) : Fin i → G.D := + G.preWalk W hi + +/-- The steps of a walk after position `i`. -/ +def segSuf {ℓ : ℕ} (W : Fin ℓ → G.D) (i : ℕ) : Fin (ℓ - (i + 1)) → G.D := + fun k => W ⟨i + 1 + k.val, by have := k.isLt; omega⟩ + +/-- Glue a prefix, a crossing label and a suffix into one walk. -/ +def segGlue {ℓ i : ℕ} (p : Fin i → G.D) (d : G.D) (s : Fin (ℓ - (i + 1)) → G.D) : + Fin ℓ → G.D := + fun k => + if hk : k.val < i then p ⟨k.val, hk⟩ + else if hk2 : k.val = i then d + else s ⟨k.val - (i + 1), by have := k.isLt; omega⟩ + +@[simp] theorem segPre_segGlue {ℓ i : ℕ} (hi : i ≤ ℓ) (p : Fin i → G.D) (d : G.D) + (s : Fin (ℓ - (i + 1)) → G.D) : + G.segPre (G.segGlue p d s) hi = p := by + funext k + simp [segPre, preWalk, segGlue, k.isLt] + +@[simp] theorem segMid_segGlue {ℓ i : ℕ} (hi : i < ℓ) (p : Fin i → G.D) (d : G.D) + (s : Fin (ℓ - (i + 1)) → G.D) : + G.segGlue p d s ⟨i, hi⟩ = d := by + simp [segGlue] + +@[simp] theorem segSuf_segGlue {ℓ i : ℕ} (p : Fin i → G.D) (d : G.D) + (s : Fin (ℓ - (i + 1)) → G.D) : + G.segSuf (G.segGlue p d s) i = s := by + funext k + have hk := k.isLt + have h1 : ¬ (i + 1 + k.val < i) := by omega + have h2 : ¬ (i + 1 + k.val = i) := by omega + simp only [segSuf, segGlue, dif_neg h1, dif_neg h2] + congr 1 + have : i + 1 + k.val - (i + 1) = k.val := by omega + exact Fin.ext this + +/-- Splitting a walk and gluing it back returns the walk. -/ +theorem segGlue_split {ℓ i : ℕ} (hi : i < ℓ) (W : Fin ℓ → G.D) : + G.segGlue (G.segPre W (le_of_lt hi)) (W ⟨i, hi⟩) (G.segSuf W i) = W := by + funext k + rcases lt_trichotomy k.val i with hk | hk | hk + · simp [segGlue, segPre, preWalk, hk] + · have hkey : k = ⟨i, hi⟩ := Fin.ext hk + subst hkey + simp + · have h1 : ¬ (k.val < i) := by omega + have h2 : ¬ (k.val = i) := by omega + simp only [segGlue, dif_neg h1, dif_neg h2, segSuf] + congr 1 + have : i + 1 + (k.val - (i + 1)) = k.val := by omega + exact Fin.ext this + +/-- The suffix walk, started at the vertex the original walk reaches at step +`i + 1`, tracks the original walk. -/ +theorem walkAt_segSuf {ℓ : ℕ} (v : G.V) (W : Fin ℓ → G.D) (i : ℕ) : + ∀ m : ℕ, m ≤ ℓ - (i + 1) → + G.walkAt (ℓ - (i + 1)) (G.walkAt ℓ v W (i + 1)) (G.segSuf W i) m + = G.walkAt ℓ v W (i + 1 + m) := by + intro m + induction m with + | zero => intro _; simp + | succ m ih => + intro hm + have hm' : m ≤ ℓ - (i + 1) := by omega + have hmlt : m < ℓ - (i + 1) := by omega + have hilt : i + 1 + m < ℓ := by omega + have hidx : i + 1 + (m + 1) = (i + 1 + m) + 1 := by omega + rw [walkAt, dif_pos hmlt, ih hm', hidx, G.walkAt_succ_of_lt v W hilt] + exact congrArg (G.nbr (G.walkAt ℓ v W (i + 1 + m))) rfl + +/-- **Reversal commutes with splitting.** Reversing the suffix of a walk gives +the prefix of the reversed walk: both are the walk that runs from the far end +back to the crossing point. This is what makes the suffix condition in the +powered constraint the same as a condition on walks out of the dart's head. -/ +theorem revWalk_segSuf {ℓ : ℕ} (v : G.V) (W : Fin ℓ → G.D) {i : ℕ} (hi : i < ℓ) : + G.revWalk (G.walkAt ℓ v W (i + 1)) (G.segSuf W i) + = G.segPre (G.revWalk v W) (show ℓ - (i + 1) ≤ ℓ by omega) := by + funext k + have hkl : k.val < ℓ := by omega + have hlt : ℓ - (k.val + 1) < ℓ := by omega + have hrev : ((Fin.rev k : Fin (ℓ - (i + 1)))).val = ℓ - (i + 1) - (k.val + 1) := + Fin.val_rev k + have hbound : (Fin.rev k : Fin (ℓ - (i + 1))).val ≤ ℓ - (i + 1) := + le_of_lt (Fin.rev k).isLt + have hpairL : (G.walkAt (ℓ - (i + 1)) (G.walkAt ℓ v W (i + 1)) (G.segSuf W i) + (Fin.rev k).val, G.segSuf W i (Fin.rev k)) + = (G.walkAt ℓ v W (ℓ - (k.val + 1)), W ⟨ℓ - (k.val + 1), hlt⟩) := by + refine Prod.ext ?_ ?_ + · have hidx : i + 1 + (Fin.rev k : Fin (ℓ - (i + 1))).val = ℓ - (k.val + 1) := by + rw [hrev] + omega + rw [G.walkAt_segSuf v W i _ hbound, hidx] + · have hidx : i + 1 + (Fin.rev k : Fin (ℓ - (i + 1))).val = ℓ - (k.val + 1) := by + rw [hrev] + omega + show G.segSuf W i (Fin.rev k) = W ⟨ℓ - (k.val + 1), hlt⟩ + rw [segSuf] + apply congrArg + exact Fin.ext hidx + have hrev2 : ((Fin.rev (⟨k.val, hkl⟩ : Fin ℓ))).val = ℓ - (k.val + 1) := + Fin.val_rev _ + have hpairR : (G.walkAt ℓ v W (Fin.rev (⟨k.val, hkl⟩ : Fin ℓ)).val, + W (Fin.rev (⟨k.val, hkl⟩ : Fin ℓ))) + = (G.walkAt ℓ v W (ℓ - (k.val + 1)), W ⟨ℓ - (k.val + 1), hlt⟩) := by + refine Prod.ext ?_ ?_ + · rw [hrev2] + · exact congrArg W (Fin.ext hrev2) + show G.revWalk (G.walkAt ℓ v W (i + 1)) (G.segSuf W i) k = G.revWalk v W ⟨k.val, hkl⟩ + simp only [revWalk, backLabel] + rw [hpairL, hpairR] + +/-! ### The crossing bijection -/ + +/-- **The crossing decomposition.** The walks of length `ℓ` that cross a given +dart `(a, d)` at step `i` are in bijection with pairs of a walk *out of* `a` of +length `i` — the reversed prefix — and a walk of length `ℓ - (i+1)` — the +suffix. Any conditions imposed on the two pieces therefore contribute +independent factors, which is what makes the first moment factorise. -/ +theorem card_crossing_eq {ℓ i : ℕ} (hi : i < ℓ) (a : G.V) (d : G.D) + (Pre : (Fin i → G.D) → Prop) [DecidablePred Pre] + (Suf : (Fin (ℓ - (i + 1)) → G.D) → Prop) [DecidablePred Suf] : + (Finset.univ.filter fun x : G.V × (Fin ℓ → G.D) => + G.walkAt ℓ x.1 x.2 i = a ∧ x.2 ⟨i, hi⟩ = d + ∧ Pre (G.revWalk x.1 (G.segPre x.2 (le_of_lt hi))) ∧ Suf (G.segSuf x.2 i)).card + = (Finset.univ.filter Pre).card * (Finset.univ.filter Suf).card := by + classical + rw [← Finset.card_product] + refine Finset.card_bij' + (fun x _ => (G.revWalk x.1 (G.segPre x.2 (le_of_lt hi)), G.segSuf x.2 i)) + (fun y _ => (G.walkEnd i a y.1, G.segGlue (G.revWalk a y.1) d y.2)) ?_ ?_ ?_ ?_ + · intro x hx + simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hx + simp only [Finset.mem_product, Finset.mem_filter, Finset.mem_univ, true_and] + exact ⟨hx.2.2.1, hx.2.2.2⟩ + · intro y hy + simp only [Finset.mem_product, Finset.mem_filter, Finset.mem_univ, true_and] at hy + simp only [Finset.mem_filter, Finset.mem_univ, true_and] + have hp : G.segPre (G.segGlue (G.revWalk a y.1) d y.2) (le_of_lt hi) + = G.revWalk a y.1 := G.segPre_segGlue (le_of_lt hi) _ d _ + refine ⟨?_, ?_, ?_, ?_⟩ + · rw [G.walkAt_eq_walkEnd_prefix _ _ i (le_of_lt hi)] + have hpre : (fun j : Fin i => + G.segGlue (G.revWalk a y.1) d y.2 (Fin.castLE (le_of_lt hi) j)) + = G.revWalk a y.1 := hp + rw [hpre] + exact G.walkEnd_revWalk a y.1 + · exact G.segMid_segGlue hi _ d _ + · rw [hp, G.revWalk_revWalk] + exact hy.1 + · rw [G.segSuf_segGlue] + exact hy.2 + · intro x hx + simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hx + obtain ⟨hwalk, hmid, -, -⟩ := hx + have ha : G.walkEnd i x.1 (G.segPre x.2 (le_of_lt hi)) = a := by + rw [← hwalk, G.walkAt_eq_walkEnd_prefix _ _ i (le_of_lt hi)] + rfl + refine Prod.ext ?_ ?_ + · show G.walkEnd i a (G.revWalk x.1 (G.segPre x.2 (le_of_lt hi))) = x.1 + rw [← ha] + exact G.walkEnd_revWalk x.1 (G.segPre x.2 (le_of_lt hi)) + · show G.segGlue (G.revWalk a (G.revWalk x.1 (G.segPre x.2 (le_of_lt hi)))) d + (G.segSuf x.2 i) = x.2 + rw [← ha, G.revWalk_revWalk, ← hmid] + exact G.segGlue_split hi x.2 + · intro y hy + simp only [Finset.mem_product, Finset.mem_filter, Finset.mem_univ, true_and] at hy + have hp : G.segPre (G.segGlue (G.revWalk a y.1) d y.2) (le_of_lt hi) + = G.revWalk a y.1 := G.segPre_segGlue (le_of_lt hi) _ d _ + refine Prod.ext ?_ ?_ + · show G.revWalk (G.walkEnd i a y.1) + (G.segPre (G.segGlue (G.revWalk a y.1) d y.2) (le_of_lt hi)) = y.1 + rw [hp, G.revWalk_revWalk] + · show G.segSuf (G.segGlue (G.revWalk a y.1) d y.2) i = y.2 + exact G.segSuf_segGlue _ d _ + +/-! ### From labels to walks -/ + +/-- **Labels grouped by their effective walk.** Any condition on the starting +vertex and the effective walk is counted by counting *walks*, each weighted by +the number of labels carrying it. This is the step that turns a statement about +killed-walk labels into one about walks, where the crossing decomposition +applies. -/ +theorem card_label_fibre {T q ℓ : ℕ} (hq : 0 < q) (hℓ : ℓ < T) + (P : G.V × (Fin ℓ → G.D) → Prop) [DecidablePred P] : + (Finset.univ.filter fun z : G.V × ((Fin T → G.D) × (Fin T → Fin q)) => + stopAt z.2.2 = ℓ ∧ P (z.1, G.preWalk z.2.1 (le_of_lt hℓ))).card + = (Finset.univ.filter P).card + * (G.deg ^ (T - ℓ) * ((q - 1) ^ ℓ * q ^ (T - ℓ - 1))) := by + classical + have hmaps : ∀ z ∈ (Finset.univ.filter fun z : G.V × ((Fin T → G.D) × (Fin T → Fin q)) => + stopAt z.2.2 = ℓ ∧ P (z.1, G.preWalk z.2.1 (le_of_lt hℓ))), + (z.1, G.preWalk z.2.1 (le_of_lt hℓ)) ∈ Finset.univ.filter P := by + intro z hz + simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hz ⊢ + exact hz.2 + rw [Finset.card_eq_sum_card_fiberwise hmaps] + have hfib : ∀ y ∈ Finset.univ.filter P, + ((Finset.univ.filter fun z : G.V × ((Fin T → G.D) × (Fin T → Fin q)) => + stopAt z.2.2 = ℓ ∧ P (z.1, G.preWalk z.2.1 (le_of_lt hℓ))).filter + fun z => (z.1, G.preWalk z.2.1 (le_of_lt hℓ)) = y).card + = G.deg ^ (T - ℓ) * ((q - 1) ^ ℓ * q ^ (T - ℓ - 1)) := by + intro y hy + simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hy + have hset : ((Finset.univ.filter fun z : G.V × ((Fin T → G.D) × (Fin T → Fin q)) => + stopAt z.2.2 = ℓ ∧ P (z.1, G.preWalk z.2.1 (le_of_lt hℓ))).filter + fun z => (z.1, G.preWalk z.2.1 (le_of_lt hℓ)) = y) + = ({y.1} : Finset G.V) ×ˢ + (Finset.univ.filter fun x : (Fin T → G.D) × (Fin T → Fin q) => + G.preWalk x.1 (le_of_lt hℓ) = y.2 ∧ stopAt x.2 = ℓ) := by + ext z + simp only [Finset.mem_filter, Finset.mem_univ, true_and, Finset.mem_product, + Finset.mem_singleton, Prod.ext_iff] + constructor + · rintro ⟨⟨hlen, -⟩, hz1, hz2⟩ + exact ⟨hz1, hz2, hlen⟩ + · rintro ⟨hz1, hz2, hlen⟩ + refine ⟨⟨hlen, ?_⟩, hz1, hz2⟩ + have hpair : (z.1, G.preWalk z.2.1 (le_of_lt hℓ)) = y := Prod.ext hz1 hz2 + rw [hpair] + exact hy + rw [hset, Finset.card_product, Finset.card_singleton, one_mul, + G.card_killed_fibre hq hℓ y.2] + rw [Finset.sum_congr rfl hfib, Finset.sum_const, smul_eq_mul] + +/-- **The crossing count, at the level of labels.** Chaining the two previous +lemmas: the killed-walk labels of effective length `ℓ` whose walk crosses the +dart `(a, d)` at step `i`, subject to any conditions on the reversed prefix and +on the suffix, number + +`(prefix count) * (suffix count) * (fibre weight)`. + +The two conditions never interact — that is the independence the killed law +buys, and the reason the first moment factorises into two plurality bounds. -/ +theorem card_label_crossing {T q ℓ i : ℕ} (hq : 0 < q) (hℓ : ℓ < T) (hi : i < ℓ) + (a : G.V) (d : G.D) + (Pre : (Fin i → G.D) → Prop) [DecidablePred Pre] + (Suf : (Fin (ℓ - (i + 1)) → G.D) → Prop) [DecidablePred Suf] : + (Finset.univ.filter fun z : G.V × ((Fin T → G.D) × (Fin T → Fin q)) => + stopAt z.2.2 = ℓ ∧ + (G.walkAt ℓ z.1 (G.preWalk z.2.1 (le_of_lt hℓ)) i = a + ∧ (G.preWalk z.2.1 (le_of_lt hℓ)) ⟨i, hi⟩ = d + ∧ Pre (G.revWalk z.1 (G.segPre (G.preWalk z.2.1 (le_of_lt hℓ)) (le_of_lt hi))) + ∧ Suf (G.segSuf (G.preWalk z.2.1 (le_of_lt hℓ)) i))).card + = (Finset.univ.filter Pre).card * (Finset.univ.filter Suf).card + * (G.deg ^ (T - ℓ) * ((q - 1) ^ ℓ * q ^ (T - ℓ - 1))) := by + classical + rw [G.card_label_fibre hq hℓ (fun y : G.V × (Fin ℓ → G.D) => + G.walkAt ℓ y.1 y.2 i = a ∧ y.2 ⟨i, hi⟩ = d + ∧ Pre (G.revWalk y.1 (G.segPre y.2 (le_of_lt hi))) + ∧ Suf (G.segSuf y.2 i)), + G.card_crossing_eq hi a d Pre Suf] + +/-- A walk following a prefix of a label sequence agrees with the full walk for +as long as the prefix lasts. This identifies the crossings of a *killed* walk +with those of the underlying fixed-length walk, which is what lets the +correlation bound — stated for fixed length — apply to them. -/ +theorem walkAt_preWalk {T ℓ : ℕ} (h : ℓ ≤ T) (x : G.V) (s : Fin T → G.D) : + ∀ k : ℕ, k ≤ ℓ → G.walkAt ℓ x (G.preWalk s h) k = G.walkAt T x s k := by + intro k + induction k with + | zero => intro _; simp + | succ k ih => + intro hk + have hkl : k < ℓ := by omega + have hkT : k < T := by omega + rw [walkAt, dif_pos hkl, ih (by omega), walkAt, dif_pos hkT] + congr 1 + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/ZigZag.lean b/Complexitylib/Classes/PCP/Internal/ZigZag.lean new file mode 100644 index 00000000..c28025b4 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/ZigZag.lean @@ -0,0 +1,783 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.RegularGraph +public import Complexitylib.Classes.PCP.Internal.Mixing +public import Complexitylib.Classes.PCP.Internal.Cheeger + +/-! +# The zig-zag product + +The expander family of `ExpanderExists` is obtained by counting, so nothing +computes it. A verifier, though, has to *build* its constraint graph, so the +`NP ⊆ PCP` direction needs a family some algorithm produces. + +The zig-zag product is the standard route, and it is enough that its *base* +graph be non-constructive: the base is a single graph of constant size, which an +algorithm may carry as a table, while the family itself is built from it by an +explicit recursion. Classically, "there is a machine with this table built in" +is provable without knowing the table. + +A vertex of `G ⓩ H` is a dart of `G` — a vertex of `G` together with one of its +labels — and a step takes three: a step in the small graph `H` on the label, a +step in `G` along the label reached, and a step in `H` on the label arrived at. +Reversing a zig-zag step reverses each of the three and swaps the two `H`-labels, +which is why the rotation map is an involution. + +## Main definitions + +- `Complexity.RegGraph.zigzag` — the product + +## Main results + +- `Complexity.RegGraph.order_zigzag`, `Complexity.RegGraph.deg_zigzag` — its size + and degree +- `Complexity.RegGraph.step_zigzag` — the walk factors as cloud, cross, cloud +- `Complexity.RegGraph.cloudStep_apply` — the cloud move *is* `H`'s own walk +- `Complexity.RegGraph.sum_sq_cloudStep_le` — so it contracts what `H` contracts +- `Complexity.RegGraph.cloudStep_cloudPar` — the cloud move fixes the part + constant along clouds, and `Complexity.RegGraph.sum_cloudPerp` — kills the rest +- `Complexity.RegGraph.ip_cloudStep`, `Complexity.RegGraph.ip_crossStep` — both + moves are self-adjoint +- `Complexity.RegGraph.cloudStep_decomp` — one cloud move splits `f` into its + constant part and a contracted remainder +- `Complexity.RegGraph.sum_mul_step_le_of_spectralBound` — a spectral bound + controls the Rayleigh quotient +- `Complexity.RegGraph.ip_cloudPar_cloudPerp` — the two parts are orthogonal +- `Complexity.RegGraph.ip_two_mul_le` — the weighted arithmetic-geometric bound +- `Complexity.RegGraph.abs_ip_step_zigzag_le` — the RVW estimate, two-sided +- `Complexity.RegGraph.spectralBound_zigzag` — **the spectral bound of the + product** +-/ + +@[expose] public section + +namespace Complexity + +namespace RegGraph + +variable (G H : RegGraph) (e : H.V ≃ G.D) + +/-- One zig-zag step: turn inside the cloud, cross, then turn again. -/ +def zigzagRot : (G.V × G.D) × (H.D × H.D) → (G.V × G.D) × (H.D × H.D) := + fun x => + let p := H.rot (e.symm x.1.2, x.2.1) + let q := G.rot (x.1.1, e p.1) + let r := H.rot (e.symm q.2, x.2.2) + ((q.1, e r.1), (r.2, p.2)) + +theorem zigzagRot_involutive : Function.Involutive (zigzagRot G H e) := by + intro x + obtain ⟨⟨v, i⟩, ⟨a, b⟩⟩ := x + simp only [zigzagRot] + -- the three reversals, innermost first + set p := H.rot (e.symm i, a) with hp + set q := G.rot (v, e p.1) with hq + set r := H.rot (e.symm q.2, b) with hr + have hrr : H.rot (e.symm (e r.1), r.2) = (e.symm q.2, b) := by + rw [Equiv.symm_apply_apply] + have : (r.1, r.2) = r := rfl + rw [this, hr, H.rot_involutive] + have hqq : G.rot (q.1, e (e.symm q.2)) = (v, e p.1) := by + rw [Equiv.apply_symm_apply] + have : (q.1, q.2) = q := rfl + rw [this, hq, G.rot_involutive] + have hpp : H.rot (e.symm (e p.1), p.2) = (e.symm i, a) := by + rw [Equiv.symm_apply_apply] + have : (p.1, p.2) = p := rfl + rw [this, hp, H.rot_involutive] + simp only [hrr, hqq, hpp] + simp + +/-- **The zig-zag product.** Its vertices are the darts of `G` and its degree is +the square of `H`'s. -/ +def zigzag : RegGraph where + V := G.V × G.D + D := H.D × H.D + decEqV := by + haveI := G.decEqV + haveI := G.decEqD + exact inferInstance + decEqD := by + haveI := H.decEqD + exact inferInstance + fintypeV := by + haveI := G.fintypeV + haveI := G.fintypeD + exact inferInstance + fintypeD := by + haveI := H.fintypeD + exact inferInstance + nonemptyD := by + haveI := H.nonemptyD + exact inferInstance + rot := zigzagRot G H e + rot_involutive := zigzagRot_involutive G H e + +@[simp] theorem order_zigzag : (zigzag G H e).order = G.order * G.deg := + @Fintype.card_prod G.V G.D G.fintypeV G.fintypeD + +@[simp] theorem deg_zigzag : (zigzag G H e).deg = H.deg * H.deg := + @Fintype.card_prod H.D H.D H.fintypeD H.fintypeD + +/-! ### The walk, factored -/ + +/-- The move inside a cloud: one step of `H` on the label, the vertex of `G` +held fixed. -/ +noncomputable def cloudStep (f : G.V × G.D → ℝ) : G.V × G.D → ℝ := + fun x => (∑ b : H.D, f (x.1, e (H.rot (e.symm x.2, b)).1)) / (H.deg : ℝ) + +/-- The crossing move: follow the dart of `G` the label names. It is composition +with `G.rot`, an involution of darts, so it merely permutes the vertices of the +product. -/ +def crossStep (f : G.V × G.D → ℝ) : G.V × G.D → ℝ := fun x => f (G.rot x) + +/-- **The zig-zag walk is cloud, then cross, then cloud.** -/ +theorem step_zigzag (f : (zigzag G H e).V → ℝ) (x : (zigzag G H e).V) : + (zigzag G H e).step f x + = cloudStep G H e (crossStep G (cloudStep G H e f)) x := by + have hd : (H.deg : ℝ) ≠ 0 := H.deg_ne_zero + have hdeg : ((zigzag G H e).deg : ℝ) = (H.deg : ℝ) * (H.deg : ℝ) := by + rw [deg_zigzag] + push_cast + ring + have hsum : (∑ d : (zigzag G H e).D, f ((zigzag G H e).nbr x d)) + = ∑ a : H.D, ∑ b : H.D, f ((zigzagRot G H e (x, (a, b))).1) := + Fintype.sum_prod_type (f := fun d : H.D × H.D => f ((zigzagRot G H e (x, d)).1)) + rw [RegGraph.step, hdeg, hsum, cloudStep] + have hinner : ∀ a : H.D, + ∑ b : H.D, f ((zigzagRot G H e (x, (a, b))).1) + = (H.deg : ℝ) * cloudStep G H e f + (G.rot (x.1, e (H.rot (e.symm x.2, a)).1)) := by + intro a + rw [cloudStep] + field_simp + rfl + rw [Finset.sum_congr rfl fun a _ => hinner a, ← Finset.mul_sum] + show _ = (∑ b : H.D, cloudStep G H e f (G.rot (x.1, e (H.rot (e.symm x.2, b)).1))) + / (H.deg : ℝ) + field_simp + +/-- The crossing move preserves the inner product with itself. -/ +theorem sum_sq_crossStep_aux (f : G.V × G.D → ℝ) : + ∑ x : G.V × G.D, crossStep G f x * crossStep G f x + = ∑ x : G.V × G.D, f x * f x := + Fintype.sum_equiv (G.rot_involutive.toPerm) + (fun x => crossStep G f x * crossStep G f x) (fun x => f x * f x) fun _ => rfl + +/-! ### The cloud move is `H`'s walk -/ + +/-- One cloud of the product, read as a function on `H`'s vertices. -/ +def cloudFun (f : G.V × G.D → ℝ) (v : G.V) : H.V → ℝ := fun u => f (v, e u) + +/-- **The cloud move is `H`'s walk**, transported along `e`. Every property of +`H`'s step operator therefore holds cloud by cloud. -/ +theorem cloudStep_apply (f : G.V × G.D → ℝ) (v : G.V) (i : G.D) : + cloudStep G H e f (v, i) = H.step (cloudFun G H e f v) (e.symm i) := rfl + +theorem sum_cloudFun (f : G.V × G.D → ℝ) (v : G.V) : + ∑ u : H.V, cloudFun G H e f v u = ∑ i : G.D, f (v, i) := + Fintype.sum_equiv e (fun u => cloudFun G H e f v u) (fun i => f (v, i)) fun _ => rfl + +theorem sum_sq_cloudFun (f : G.V × G.D → ℝ) (v : G.V) : + ∑ u : H.V, (cloudFun G H e f v u) ^ 2 = ∑ i : G.D, (f (v, i)) ^ 2 := + Fintype.sum_equiv e (fun u => (cloudFun G H e f v u) ^ 2) (fun i => (f (v, i)) ^ 2) + fun _ => rfl + +/-- **The cloud move contracts what `H` contracts.** On a cloud whose values sum +to zero, one cloud move shrinks the sum of squares by `lam ^ 2`. -/ +theorem sum_sq_cloudStep_le {lam : ℝ} (hH : H.SpectralBound lam) + (f : G.V × G.D → ℝ) (v : G.V) (hv : ∑ i : G.D, f (v, i) = 0) : + ∑ i : G.D, (cloudStep G H e f (v, i)) ^ 2 ≤ lam ^ 2 * ∑ i : G.D, (f (v, i)) ^ 2 := by + have hzero : ∑ u : H.V, cloudFun G H e f v u = 0 := by + rw [sum_cloudFun] + exact hv + have hbound := hH (cloudFun G H e f v) hzero + have h : ∑ i : G.D, (cloudStep G H e f (v, i)) ^ 2 + = ∑ u : H.V, (H.step (cloudFun G H e f v) u) ^ 2 := + (Fintype.sum_equiv e (fun u => (H.step (cloudFun G H e f v) u) ^ 2) + (fun i => (cloudStep G H e f (v, i)) ^ 2) fun u => by + dsimp only + rw [cloudStep_apply, Equiv.symm_apply_apply]).symm + rw [h, ← sum_sq_cloudFun] + exact hbound + +/-! ### Splitting off the part constant along clouds -/ + +/-- The average of `f` over the cloud above a vertex of `G`. -/ +noncomputable def cloudMean (f : G.V × G.D → ℝ) (v : G.V) : ℝ := + (∑ i : G.D, f (v, i)) / (G.deg : ℝ) + +/-- The part of `f` that is constant along each cloud. -/ +noncomputable def cloudPar (f : G.V × G.D → ℝ) : G.V × G.D → ℝ := + fun x => cloudMean G f x.1 + +/-- What is left over. -/ +noncomputable def cloudPerp (f : G.V × G.D → ℝ) : G.V × G.D → ℝ := + fun x => f x - cloudPar G f x + +theorem cloudPar_add_cloudPerp (f : G.V × G.D → ℝ) (x : G.V × G.D) : + cloudPar G f x + cloudPerp G f x = f x := by + rw [cloudPerp] + ring + +/-- **The cloud move fixes the constant part.** -/ +theorem cloudStep_cloudPar (f : G.V × G.D → ℝ) (x : G.V × G.D) : + cloudStep G H e (cloudPar G f) x = cloudPar G f x := by + obtain ⟨v, i⟩ := x + rw [cloudStep_apply] + have hconst : cloudFun G H e (cloudPar G f) v = fun _ => cloudMean G f v := rfl + rw [hconst, RegGraph.step_const] + rfl + +/-- **The leftover part sums to zero on every cloud.** -/ +theorem sum_cloudPerp (f : G.V × G.D → ℝ) (v : G.V) : + ∑ i : G.D, cloudPerp G f (v, i) = 0 := by + have hdeg : (G.deg : ℝ) ≠ 0 := G.deg_ne_zero + have hcard : (Finset.univ : Finset G.D).card = G.deg := Finset.card_univ + rw [show (fun i : G.D => cloudPerp G f (v, i)) + = fun i : G.D => f (v, i) - cloudMean G f v from rfl] + rw [Finset.sum_sub_distrib, Finset.sum_const, hcard, nsmul_eq_mul, cloudMean] + field_simp + ring + +/-- The cloud move contracts the leftover part. -/ +theorem sum_sq_cloudStep_cloudPerp_le {lam : ℝ} (hH : H.SpectralBound lam) + (f : G.V × G.D → ℝ) (v : G.V) : + ∑ i : G.D, (cloudStep G H e (cloudPerp G f) (v, i)) ^ 2 + ≤ lam ^ 2 * ∑ i : G.D, (cloudPerp G f (v, i)) ^ 2 := + sum_sq_cloudStep_le G H e hH (cloudPerp G f) v (sum_cloudPerp G f v) + +/-! ### Both moves are self-adjoint -/ + +/-- One step of a graph is self-adjoint: reversing darts is a bijection. -/ +theorem sum_mul_step_comm (K : RegGraph) (f g : K.V → ℝ) : + ∑ v : K.V, f v * K.step g v = ∑ v : K.V, K.step f v * g v := by + have hr : ∑ v : K.V, K.step f v * g v = ∑ v : K.V, g v * K.step f v := + Finset.sum_congr rfl fun v _ => mul_comm _ _ + rw [hr, sum_mul_step, sum_mul_step] + congr 1 + rw [K.sum_darts_swap (fun u w => f u * g w)] + exact Finset.sum_congr rfl fun p _ => mul_comm _ _ + +/-- The inner product of two functions on the darts of `G`. -/ +noncomputable def ip (f g : G.V × G.D → ℝ) : ℝ := ∑ x : G.V × G.D, f x * g x + +theorem ip_comm (f g : G.V × G.D → ℝ) : ip G f g = ip G g f := by + rw [ip, ip] + exact Finset.sum_congr rfl fun x _ => mul_comm _ _ + +/-- Summing over the product is summing cloud by cloud. -/ +theorem ip_eq_sum_clouds (f g : G.V × G.D → ℝ) : + ip G f g = ∑ v : G.V, ∑ i : G.D, f (v, i) * g (v, i) := by + rw [ip, Fintype.sum_prod_type] + +/-- **The cloud move is self-adjoint**, because `H`'s walk is. -/ +theorem ip_cloudStep (f g : G.V × G.D → ℝ) : + ip G (cloudStep G H e f) g = ip G f (cloudStep G H e g) := by + rw [ip_eq_sum_clouds, ip_eq_sum_clouds] + refine Finset.sum_congr rfl fun v _ => ?_ + have hl : ∑ i : G.D, cloudStep G H e f (v, i) * g (v, i) + = ∑ u : H.V, H.step (cloudFun G H e f v) u * cloudFun G H e g v u := + (Fintype.sum_equiv e + (fun u => H.step (cloudFun G H e f v) u * cloudFun G H e g v u) + (fun i => cloudStep G H e f (v, i) * g (v, i)) fun u => by + dsimp only + rw [cloudStep_apply, Equiv.symm_apply_apply] + rfl).symm + have hr : ∑ i : G.D, f (v, i) * cloudStep G H e g (v, i) + = ∑ u : H.V, cloudFun G H e f v u * H.step (cloudFun G H e g v) u := + (Fintype.sum_equiv e + (fun u => cloudFun G H e f v u * H.step (cloudFun G H e g v) u) + (fun i => f (v, i) * cloudStep G H e g (v, i)) fun u => by + dsimp only + rw [cloudStep_apply, Equiv.symm_apply_apply] + rfl).symm + rw [hl, hr, ← sum_mul_step_comm] + +/-- **The crossing move is self-adjoint**, because `G.rot` is an involution. -/ +theorem ip_crossStep (f g : G.V × G.D → ℝ) : + ip G (crossStep G f) g = ip G f (crossStep G g) := by + rw [ip, ip] + refine Fintype.sum_equiv (G.rot_involutive.toPerm) + (fun x => crossStep G f x * g x) (fun x => f x * crossStep G g x) fun x => ?_ + show f (G.rot x) * g x = f (G.rot x) * g (G.rot (G.rot x)) + rw [G.rot_involutive x] + +/-! ### The constant part is a function on `G` -/ + +/-- The inner product of two cloud-constant functions is `G.deg` times the inner +product of the functions they come from. -/ +theorem ip_cloudPar (f g : G.V × G.D → ℝ) : + ip G (cloudPar G f) (cloudPar G g) + = (G.deg : ℝ) * ∑ v : G.V, cloudMean G f v * cloudMean G g v := by + rw [ip_eq_sum_clouds, Finset.mul_sum] + refine Finset.sum_congr rfl fun v _ => ?_ + have hconst : ∀ i : G.D, cloudPar G f (v, i) * cloudPar G g (v, i) + = cloudMean G f v * cloudMean G g v := fun _ => rfl + rw [Finset.sum_congr rfl fun i _ => hconst i, Finset.sum_const, Finset.card_univ, + nsmul_eq_mul] + rfl + +/-- A mean-zero function on the product has mean-zero cloud averages. -/ +theorem sum_cloudMean (f : G.V × G.D → ℝ) : + ∑ v : G.V, cloudMean G f v = (∑ x : G.V × G.D, f x) / (G.deg : ℝ) := by + rw [Fintype.sum_prod_type, Finset.sum_div] + exact Finset.sum_congr rfl fun v _ => rfl + +theorem sum_cloudMean_eq_zero {f : G.V × G.D → ℝ} (hf : ∑ x : G.V × G.D, f x = 0) : + ∑ v : G.V, cloudMean G f v = 0 := by + rw [sum_cloudMean, hf, zero_div] + +/-- The sum of squares of the constant part, in terms of `G`. -/ +theorem ip_cloudPar_self (f : G.V × G.D → ℝ) : + ip G (cloudPar G f) (cloudPar G f) + = (G.deg : ℝ) * ∑ v : G.V, (cloudMean G f v) ^ 2 := by + rw [ip_cloudPar] + congr 1 + exact Finset.sum_congr rfl fun v _ => (sq _).symm + +/-- **The parallel–parallel term is `G`'s own quadratic form.** This is where +`G.SpectralBound` will enter the estimate. -/ +theorem ip_cloudPar_crossStep (f : G.V × G.D → ℝ) : + ip G (cloudPar G f) (crossStep G (cloudPar G f)) + = (G.deg : ℝ) * ∑ v : G.V, cloudMean G f v * G.step (cloudMean G f) v := by + have hdeg : (G.deg : ℝ) ≠ 0 := G.deg_ne_zero + rw [ip_eq_sum_clouds, Finset.mul_sum] + refine Finset.sum_congr rfl fun v _ => ?_ + have h : ∀ i : G.D, cloudPar G f (v, i) * crossStep G (cloudPar G f) (v, i) + = cloudMean G f v * cloudMean G f (G.nbr v i) := fun _ => rfl + rw [Finset.sum_congr rfl fun i _ => h i, ← Finset.mul_sum, RegGraph.step] + field_simp + +/-! ### Linearity -/ + +theorem cloudStep_add (f g : G.V × G.D → ℝ) (x : G.V × G.D) : + cloudStep G H e (fun y => f y + g y) x = cloudStep G H e f x + cloudStep G H e g x := by + rw [cloudStep, cloudStep, cloudStep, ← add_div] + congr 1 + exact Finset.sum_add_distrib + +theorem crossStep_add (f g : G.V × G.D → ℝ) (x : G.V × G.D) : + crossStep G (fun y => f y + g y) x = crossStep G f x + crossStep G g x := rfl + +theorem ip_add_left (f g h : G.V × G.D → ℝ) : + ip G (fun x => f x + g x) h = ip G f h + ip G g h := by + rw [ip, ip, ip, ← Finset.sum_add_distrib] + exact Finset.sum_congr rfl fun x _ => by ring + +theorem ip_add_right (f g h : G.V × G.D → ℝ) : + ip G f (fun x => g x + h x) = ip G f g + ip G f h := by + rw [ip, ip, ip, ← Finset.sum_add_distrib] + exact Finset.sum_congr rfl fun x _ => by ring + +/-- **One cloud move splits into the constant part and a contracted +remainder.** -/ +theorem cloudStep_decomp (f : G.V × G.D → ℝ) (x : G.V × G.D) : + cloudStep G H e f x = cloudPar G f x + cloudStep G H e (cloudPerp G f) x := by + have hsplit : f = fun y => cloudPar G f y + cloudPerp G f y := by + funext y + rw [cloudPar_add_cloudPerp] + conv_lhs => rw [hsplit] + rw [cloudStep_add, cloudStep_cloudPar] + +/-- The zig-zag walk, with the cloud move split on both sides. -/ +theorem step_zigzag_decomp (f : (zigzag G H e).V → ℝ) : + ip G f ((zigzag G H e).step f) + = ip G (fun x => cloudPar G f x + cloudStep G H e (cloudPerp G f) x) + (crossStep G (fun x => cloudPar G f x + cloudStep G H e (cloudPerp G f) x)) := by + have hstep : ∀ x, (zigzag G H e).step f x + = cloudStep G H e (crossStep G (cloudStep G H e f)) x := step_zigzag G H e f + have hz : (zigzag G H e).step f = cloudStep G H e (crossStep G (cloudStep G H e f)) := by + funext x + exact hstep x + rw [hz, ← ip_cloudStep] + have hdec : cloudStep G H e f + = fun x => cloudPar G f x + cloudStep G H e (cloudPerp G f) x := by + funext x + exact cloudStep_decomp G H e f x + rw [hdec] + +/-- **The four terms of the estimate.** -/ +theorem ip_crossStep_expand (p q : G.V × G.D → ℝ) : + ip G (fun x => p x + q x) (crossStep G (fun x => p x + q x)) + = ip G p (crossStep G p) + ip G p (crossStep G q) + + ip G q (crossStep G p) + ip G q (crossStep G q) := by + have hc : crossStep G (fun x => p x + q x) + = fun x => crossStep G p x + crossStep G q x := by + funext x + exact crossStep_add G p q x + rw [hc, ip_add_left, ip_add_right, ip_add_right] + ring + +/-- **The zig-zag quadratic form, expanded.** The first summand is `G`'s own +form on the cloud averages; the other three involve the contracted remainder. -/ +theorem ip_step_zigzag_expand (f : (zigzag G H e).V → ℝ) : + ip G f ((zigzag G H e).step f) + = ip G (cloudPar G f) (crossStep G (cloudPar G f)) + + ip G (cloudPar G f) (crossStep G (cloudStep G H e (cloudPerp G f))) + + ip G (cloudStep G H e (cloudPerp G f)) (crossStep G (cloudPar G f)) + + ip G (cloudStep G H e (cloudPerp G f)) + (crossStep G (cloudStep G H e (cloudPerp G f))) := by + rw [step_zigzag_decomp, ip_crossStep_expand] + +/-! ### The Rayleigh quotient -/ + +/-- **A spectral bound controls the quadratic form.** Cauchy–Schwarz turns the +bound on `‖step g‖` into one on `⟨g, step g⟩`, which is the form the zig-zag +estimate consumes. -/ +theorem sum_mul_step_le_of_spectralBound (K : RegGraph) {lam : ℝ} (hK : K.SpectralBound lam) + (hlam : 0 ≤ lam) (g : K.V → ℝ) (hg : ∑ v : K.V, g v = 0) : + ∑ v : K.V, g v * K.step g v ≤ lam * ∑ v : K.V, (g v) ^ 2 := by + set S := ∑ v : K.V, (g v) ^ 2 with hS + set T := ∑ v : K.V, g v * K.step g v with hT + have hS0 : 0 ≤ S := Finset.sum_nonneg fun _ _ => sq_nonneg _ + have hcs := Finset.sum_mul_sq_le_sq_mul_sq (Finset.univ : Finset K.V) g (K.step g) + have hspec := hK g hg + have hT2 : T ^ 2 ≤ S * (lam ^ 2 * S) := + le_trans hcs (mul_le_mul_of_nonneg_left hspec hS0) + nlinarith [hT2, mul_nonneg hlam hS0, sq_nonneg (T + lam * S)] + +/-- The norm of the contracted remainder, over the whole product. -/ +theorem ip_cloudStep_cloudPerp_le {lam : ℝ} (hH : H.SpectralBound lam) + (f : G.V × G.D → ℝ) : + ip G (cloudStep G H e (cloudPerp G f)) (cloudStep G H e (cloudPerp G f)) + ≤ lam ^ 2 * ip G (cloudPerp G f) (cloudPerp G f) := by + rw [ip_eq_sum_clouds, ip_eq_sum_clouds, Finset.mul_sum] + refine Finset.sum_le_sum fun v _ => ?_ + have hb := sum_sq_cloudStep_cloudPerp_le G H e hH f v + have hl : ∑ i : G.D, cloudStep G H e (cloudPerp G f) (v, i) + * cloudStep G H e (cloudPerp G f) (v, i) + = ∑ i : G.D, (cloudStep G H e (cloudPerp G f) (v, i)) ^ 2 := + Finset.sum_congr rfl fun i _ => (sq _).symm + have hr : ∑ i : G.D, cloudPerp G f (v, i) * cloudPerp G f (v, i) + = ∑ i : G.D, (cloudPerp G f (v, i)) ^ 2 := + Finset.sum_congr rfl fun i _ => (sq _).symm + rw [hl, hr] + exact hb + +/-! ### Orthogonality and a weighted bound -/ + +theorem ip_nonneg (g : G.V × G.D → ℝ) : 0 ≤ ip G g g := + Finset.sum_nonneg fun _ _ => mul_self_nonneg _ + +/-- **The two parts are orthogonal.** The constant part is fixed along a cloud +while the remainder sums to zero there. -/ +theorem ip_cloudPar_cloudPerp (f : G.V × G.D → ℝ) : + ip G (cloudPar G f) (cloudPerp G f) = 0 := by + rw [ip_eq_sum_clouds] + refine Finset.sum_eq_zero fun v _ => ?_ + have h : ∀ i : G.D, cloudPar G f (v, i) * cloudPerp G f (v, i) + = cloudMean G f v * cloudPerp G f (v, i) := fun _ => rfl + rw [Finset.sum_congr rfl fun i _ => h i, ← Finset.mul_sum, sum_cloudPerp, mul_zero] + +/-- **Pythagoras** for the splitting. -/ +theorem ip_self_split (f : G.V × G.D → ℝ) : + ip G f f = ip G (cloudPar G f) (cloudPar G f) + ip G (cloudPerp G f) (cloudPerp G f) := by + have hsplit : f = fun y => cloudPar G f y + cloudPerp G f y := by + funext y + rw [cloudPar_add_cloudPerp] + conv_lhs => rw [hsplit] + rw [ip_add_left, ip_add_right, ip_add_right, ip_cloudPar_cloudPerp, + ip_comm G (cloudPerp G f) (cloudPar G f), ip_cloudPar_cloudPerp] + ring + +/-- **The weighted arithmetic-geometric bound.** Expanding `0 ≤ ‖t u - w‖²` +avoids any square root, which keeps the estimate inside the ordered field. -/ +theorem ip_two_mul_le (u w : G.V × G.D → ℝ) {t : ℝ} (ht : 0 < t) : + 2 * ip G u w ≤ t * ip G u u + ip G w w / t := by + have hnn : 0 ≤ ∑ x : G.V × G.D, (t * u x - w x) ^ 2 := + Finset.sum_nonneg fun _ _ => sq_nonneg _ + have hexp : ∑ x : G.V × G.D, (t * u x - w x) ^ 2 + = t ^ 2 * ip G u u - 2 * t * ip G u w + ip G w w := by + rw [ip, ip, ip, Finset.mul_sum, Finset.mul_sum, ← Finset.sum_sub_distrib, + ← Finset.sum_add_distrib] + exact Finset.sum_congr rfl fun x _ => by ring + rw [hexp] at hnn + have hmul : 2 * ip G u w * t ≤ (t * ip G u u + ip G w w / t) * t := by + have ht' : t ≠ 0 := ne_of_gt ht + field_simp + nlinarith [hnn] + exact le_of_mul_le_mul_right hmul ht + +/-- The crossing move preserves the inner product with itself, in `ip` form. -/ +theorem ip_crossStep_self (g : G.V × G.D → ℝ) : + ip G (crossStep G g) (crossStep G g) = ip G g g := by + rw [ip, ip] + exact sum_sq_crossStep_aux G g + +/-! ### The Reingold–Vadhan–Wigderson estimate -/ + +theorem eq_zero_of_ip_self_eq_zero {g : G.V × G.D → ℝ} (h : ip G g g = 0) + (x : G.V × G.D) : g x = 0 := by + have hmem := (Finset.sum_eq_zero_iff_of_nonneg + (fun y (_ : y ∈ (Finset.univ : Finset (G.V × G.D))) => mul_self_nonneg (g y))).1 h x + (Finset.mem_univ x) + exact mul_self_eq_zero.1 hmem + +/-! ### Two-sided forms -/ + +theorem ip_neg_left (u w : G.V × G.D → ℝ) : + ip G (fun x => -u x) w = -ip G u w := by + rw [ip, ip, ← Finset.sum_neg_distrib] + exact Finset.sum_congr rfl fun x _ => by ring + +theorem ip_neg_self (u : G.V × G.D → ℝ) : + ip G (fun x => -u x) (fun x => -u x) = ip G u u := by + rw [ip, ip] + exact Finset.sum_congr rfl fun x _ => by ring + +/-- The weighted bound, two-sided. -/ +theorem abs_ip_two_mul_le (u w : G.V × G.D → ℝ) {t : ℝ} (ht : 0 < t) : + 2 * |ip G u w| ≤ t * ip G u u + ip G w w / t := by + have hpos := ip_two_mul_le G u w ht + have hneg := ip_two_mul_le G (fun x => -u x) w ht + rw [ip_neg_left, ip_neg_self] at hneg + rcases abs_cases (ip G u w) with ⟨he, -⟩ | ⟨he, -⟩ <;> rw [he] <;> linarith + +/-- **The Rayleigh quotient, two-sided.** -/ +theorem abs_sum_mul_step_le (K : RegGraph) {lam : ℝ} (hK : K.SpectralBound lam) + (hlam : 0 ≤ lam) (g : K.V → ℝ) (hg : ∑ v : K.V, g v = 0) : + |∑ v : K.V, g v * K.step g v| ≤ lam * ∑ v : K.V, (g v) ^ 2 := by + set S := ∑ v : K.V, (g v) ^ 2 with hS + set T := ∑ v : K.V, g v * K.step g v with hT + have hS0 : 0 ≤ S := Finset.sum_nonneg fun _ _ => sq_nonneg _ + have hcs := Finset.sum_mul_sq_le_sq_mul_sq (Finset.univ : Finset K.V) g (K.step g) + have hspec := hK g hg + have hT2 : T ^ 2 ≤ S * (lam ^ 2 * S) := + le_trans hcs (mul_le_mul_of_nonneg_left hspec hS0) + rw [abs_le] + constructor + · nlinarith [hT2, mul_nonneg hlam hS0, sq_nonneg (T - lam * S)] + · nlinarith [hT2, mul_nonneg hlam hS0, sq_nonneg (T + lam * S)] + +/-- **Term one, two-sided.** -/ +theorem abs_ip_cloudPar_crossStep_le {lam : ℝ} (hG : G.SpectralBound lam) (hlam : 0 ≤ lam) + {f : G.V × G.D → ℝ} (hf : ∑ x : G.V × G.D, f x = 0) : + |ip G (cloudPar G f) (crossStep G (cloudPar G f))| + ≤ lam * ip G (cloudPar G f) (cloudPar G f) := by + have hzero : ∑ v : G.V, cloudMean G f v = 0 := sum_cloudMean_eq_zero G hf + have hray := abs_sum_mul_step_le G hG hlam (cloudMean G f) hzero + rw [ip_cloudPar_crossStep, ip_cloudPar_self, abs_mul, + abs_of_nonneg (show (0:ℝ) ≤ (G.deg : ℝ) by positivity)] + have hd : (0 : ℝ) ≤ (G.deg : ℝ) := by positivity + calc (G.deg : ℝ) * |∑ v : G.V, cloudMean G f v * G.step (cloudMean G f) v| + ≤ (G.deg : ℝ) * (lam * ∑ v : G.V, (cloudMean G f v) ^ 2) := + mul_le_mul_of_nonneg_left hray hd + _ = lam * ((G.deg : ℝ) * ∑ v : G.V, (cloudMean G f v) ^ 2) := by ring + +/-- **The RVW estimate, two-sided.** This is the form the conversion to +`SpectralBound` needs, since polarisation uses the bound on both signs. -/ +theorem abs_ip_step_zigzag_le {lamG lamH : ℝ} (hG : G.SpectralBound lamG) + (hH : H.SpectralBound lamH) (hlamG : 0 ≤ lamG) (hlamH : 0 ≤ lamH) + (f : (zigzag G H e).V → ℝ) (hf : ∑ x : G.V × G.D, f x = 0) : + |ip G f ((zigzag G H e).step f)| ≤ (lamG + lamH + lamH ^ 2) * ip G f f := by + set p := cloudPar G f with hp + set r := cloudPerp G f with hr + set q := cloudStep G H e r with hq + set a := ip G p p with ha + set b := ip G r r with hb + have ha0 : 0 ≤ a := ip_nonneg G p + have hb0 : 0 ≤ b := ip_nonneg G r + have hqq : ip G q q ≤ lamH ^ 2 * b := ip_cloudStep_cloudPerp_le G H e hH f + have hq0 : 0 ≤ ip G q q := ip_nonneg G q + have hsplit : ip G f f = a + b := ip_self_split G f + have hterm1 : |ip G p (crossStep G p)| ≤ lamG * a := + abs_ip_cloudPar_crossStep_le G hG hlamG hf + have hsym : ip G q (crossStep G p) = ip G p (crossStep G q) := by + rw [← ip_crossStep, ip_comm] + have hcross : 2 * |ip G p (crossStep G q)| ≤ lamH * a + lamH * b := by + rcases eq_or_lt_of_le hlamH with h0 | hpos + · have hz : ip G q q = 0 := le_antisymm (by rw [← h0] at hqq; simpa using hqq) hq0 + have hqzero : ∀ x, q x = 0 := fun x => eq_zero_of_ip_self_eq_zero G hz x + have hzero2 : ip G p (crossStep G q) = 0 := by + rw [ip] + refine Finset.sum_eq_zero fun x _ => ?_ + show p x * q (G.rot x) = 0 + rw [hqzero, mul_zero] + rw [hzero2, ← h0] + norm_num + · have hb2 := abs_ip_two_mul_le G p (crossStep G q) hpos + rw [ip_crossStep_self] at hb2 + have hdiv : ip G q q / lamH ≤ lamH * b := by + rw [div_le_iff₀ hpos] + calc ip G q q ≤ lamH ^ 2 * b := hqq + _ = lamH * b * lamH := by ring + linarith + have hterm4 : |ip G q (crossStep G q)| ≤ lamH ^ 2 * b := by + have h1 := abs_ip_two_mul_le G q (crossStep G q) (by norm_num : (0 : ℝ) < 1) + rw [ip_crossStep_self] at h1 + linarith + rw [ip_step_zigzag_expand, hsplit, ← hp, ← hr, ← hq, hsym] + have hgb : 0 ≤ lamG * b := mul_nonneg hlamG hb0 + have hha : 0 ≤ lamH ^ 2 * a := mul_nonneg (sq_nonneg _) ha0 + have hcross' : |ip G p (crossStep G q)| ≤ (lamH * a + lamH * b) / 2 := by linarith + have h1 := abs_le.1 hterm1 + have h2 := abs_le.1 hcross' + have h4 := abs_le.1 hterm4 + rw [abs_le] + constructor <;> linarith [h1.1, h1.2, h2.1, h2.2, h4.1, h4.2, hgb, hha] + +/-! ### From the quadratic form to the spectral bound -/ + +theorem step_sub (K : RegGraph) (f g : K.V → ℝ) (v : K.V) : + K.step (fun w => f w - g w) v = K.step f v - K.step g v := by + rw [RegGraph.step, RegGraph.step, RegGraph.step, ← sub_div] + congr 1 + exact Finset.sum_sub_distrib (s := (Finset.univ : Finset K.D)) + (f := fun i => f (K.nbr v i)) (g := fun i => g (K.nbr v i)) + +theorem ip_sub_left (f g h : G.V × G.D → ℝ) : + ip G (fun x => f x - g x) h = ip G f h - ip G g h := by + rw [ip, ip, ip, ← Finset.sum_sub_distrib] + exact Finset.sum_congr rfl fun x _ => by ring + +theorem ip_sub_right (f g h : G.V × G.D → ℝ) : + ip G f (fun x => g x - h x) = ip G f g - ip G f h := by + rw [ip, ip, ip, ← Finset.sum_sub_distrib] + exact Finset.sum_congr rfl fun x _ => by ring + +/-- The walk of the product is self-adjoint — it is a walk, like any other. -/ +theorem ip_step_zigzag_comm (f g : G.V × G.D → ℝ) : + ip G ((zigzag G H e).step f) g = ip G f ((zigzag G H e).step g) := by + rw [ip, ip] + exact (sum_mul_step_comm (zigzag G H e) f g).symm + +/-- The sum of squares over the product's vertices, as an inner product. -/ +theorem sum_sq_eq_ip (g : (zigzag G H e).V → ℝ) : + ∑ v : (zigzag G H e).V, (g v) ^ 2 = ip G g g := by + rw [ip] + exact Finset.sum_congr rfl fun x _ => sq _ + +/-- **Polarisation.** -/ +theorem ip_polarise (f g : G.V × G.D → ℝ) : + 4 * ip G ((zigzag G H e).step f) g + = ip G ((zigzag G H e).step (fun x => f x + g x)) (fun x => f x + g x) + - ip G ((zigzag G H e).step (fun x => f x - g x)) (fun x => f x - g x) := by + have hadd : (zigzag G H e).step (fun x => f x + g x) + = fun x => (zigzag G H e).step f x + (zigzag G H e).step g x := by + funext x + exact RegGraph.step_add (zigzag G H e) f g x + have hsub : (zigzag G H e).step (fun x => f x - g x) + = fun x => (zigzag G H e).step f x - (zigzag G H e).step g x := by + funext x + exact step_sub (zigzag G H e) f g x + have hcross : ip G ((zigzag G H e).step g) f = ip G ((zigzag G H e).step f) g := by + rw [ip_step_zigzag_comm, ip_comm] + have key : ip G ((zigzag G H e).step (fun x => f x + g x)) (fun x => f x + g x) + - ip G ((zigzag G H e).step (fun x => f x - g x)) (fun x => f x - g x) + = 2 * ip G ((zigzag G H e).step f) g + 2 * ip G ((zigzag G H e).step g) f := by + rw [hadd, hsub, ip, ip, ip, ip, ← Finset.sum_sub_distrib, Finset.mul_sum, + Finset.mul_sum, ← Finset.sum_add_distrib] + exact Finset.sum_congr rfl fun x _ => by ring + rw [key, hcross] + ring + +/-- **The spectral bound of the zig-zag product.** -/ +theorem spectralBound_zigzag {lamG lamH : ℝ} (hG : G.SpectralBound lamG) + (hH : H.SpectralBound lamH) (hlamG : 0 ≤ lamG) (hlamH : 0 ≤ lamH) : + (zigzag G H e).SpectralBound (lamG + lamH + lamH ^ 2) := by + have hlam0 : (0 : ℝ) ≤ lamG + lamH + lamH ^ 2 := by positivity + intro f hf + have hf' : ∑ x : G.V × G.D, f x = 0 := hf + have hmean : ∑ x : G.V × G.D, (zigzag G H e).step f x = ∑ x : G.V × G.D, f x := + RegGraph.sum_step (zigzag G H e) f + have hTf0 : ∑ x : G.V × G.D, (zigzag G H e).step f x = 0 := by rw [hmean]; exact hf' + have hA0 : 0 ≤ ip G ((zigzag G H e).step f) ((zigzag G H e).step f) := ip_nonneg G _ + -- the test vector `g = (1/lam) • T f`, or `T f` itself in the degenerate case + have hmain : ∀ c : ℝ, 0 < c → + 2 * (ip G ((zigzag G H e).step f) ((zigzag G H e).step f) * c) + ≤ (lamG + lamH + lamH ^ 2) * (ip G f f + + c ^ 2 * ip G ((zigzag G H e).step f) ((zigzag G H e).step f)) := by + intro c hc + set g : G.V × G.D → ℝ := fun x => c * (zigzag G H e).step f x with hg + have hg0 : ∑ x : G.V × G.D, g x = 0 := by + rw [hg, ← Finset.mul_sum, hTf0, mul_zero] + have hgg : ip G g g + = c ^ 2 * ip G ((zigzag G H e).step f) ((zigzag G H e).step f) := by + rw [hg, ip, ip, Finset.mul_sum] + exact Finset.sum_congr rfl fun x _ => by ring + have hTfg : ip G ((zigzag G H e).step f) g + = c * ip G ((zigzag G H e).step f) ((zigzag G H e).step f) := by + rw [hg, ip, ip, Finset.mul_sum] + exact Finset.sum_congr rfl fun x _ => by ring + have hpol := ip_polarise G H e f g + have h1 := abs_ip_step_zigzag_le G H e hG hH hlamG hlamH + (fun x => f x + g x) (by rw [Finset.sum_add_distrib, hf', hg0]; ring) + have h2 := abs_ip_step_zigzag_le G H e hG hH hlamG hlamH + (fun x => f x - g x) (by rw [Finset.sum_sub_distrib, hf', hg0]; ring) + have hpar : ip G (fun x => f x + g x) (fun x => f x + g x) + + ip G (fun x => f x - g x) (fun x => f x - g x) + = 2 * ip G f f + 2 * ip G g g := by + rw [ip_add_left, ip_add_right, ip_add_right, ip_sub_left, ip_sub_right, + ip_sub_right, ip_comm G g f] + ring + have hb1 := abs_le.1 h1 + have hb2 := abs_le.1 h2 + have hswap1 : ip G ((zigzag G H e).step (fun x => f x + g x)) (fun x => f x + g x) + = ip G (fun x => f x + g x) ((zigzag G H e).step (fun x => f x + g x)) := + ip_comm G _ _ + have hswap2 : ip G ((zigzag G H e).step (fun x => f x - g x)) (fun x => f x - g x) + = ip G (fun x => f x - g x) ((zigzag G H e).step (fun x => f x - g x)) := + ip_comm G _ _ + rw [hTfg, hswap1, hswap2] at hpol + rw [hgg] at hpar + have hscaled : (lamG + lamH + lamH ^ 2) + * ((ip G (fun x => f x + g x) (fun x => f x + g x)) + + ip G (fun x => f x - g x) (fun x => f x - g x)) + = (lamG + lamH + lamH ^ 2) + * (2 * ip G f f + + 2 * (c ^ 2 * ip G ((zigzag G H e).step f) ((zigzag G H e).step f))) := by + rw [hpar] + have hcomb : ip G (fun x => f x + g x) ((zigzag G H e).step (fun x => f x + g x)) + - ip G (fun x => f x - g x) ((zigzag G H e).step (fun x => f x - g x)) + ≤ (lamG + lamH + lamH ^ 2) * (ip G (fun x => f x + g x) (fun x => f x + g x)) + + (lamG + lamH + lamH ^ 2) + * (ip G (fun x => f x - g x) (fun x => f x - g x)) := by + have hq' : -(ip G (fun x => f x - g x) + ((zigzag G H e).step (fun x => f x - g x))) + ≤ (lamG + lamH + lamH ^ 2) + * (ip G (fun x => f x - g x) (fun x => f x - g x)) := + neg_le_of_neg_le hb2.1 + have hsum := add_le_add hb1.2 hq' + rw [sub_eq_add_neg] + exact hsum + linarith [hpol, hcomb, hscaled] + rcases eq_or_lt_of_le hlam0 with h0 | hpos + · have hc := hmain 1 one_pos + rw [← h0] at hc + have : ip G ((zigzag G H e).step f) ((zigzag G H e).step f) ≤ 0 := by linarith + have hz : ip G ((zigzag G H e).step f) ((zigzag G H e).step f) = 0 := + le_antisymm this hA0 + show ∑ v : (zigzag G H e).V, ((zigzag G H e).step f v) ^ 2 ≤ _ + rw [sum_sq_eq_ip, sum_sq_eq_ip, hz, ← h0] + norm_num + · have hc := hmain (1 / (lamG + lamH + lamH ^ 2)) (by positivity) + have hsimp : (lamG + lamH + lamH ^ 2) + * (ip G f f + (1 / (lamG + lamH + lamH ^ 2)) ^ 2 + * ip G ((zigzag G H e).step f) ((zigzag G H e).step f)) + = (lamG + lamH + lamH ^ 2) * ip G f f + + ip G ((zigzag G H e).step f) ((zigzag G H e).step f) + / (lamG + lamH + lamH ^ 2) := by + field_simp + rw [hsimp] at hc + have hkey : ip G ((zigzag G H e).step f) ((zigzag G H e).step f) + / (lamG + lamH + lamH ^ 2) ≤ (lamG + lamH + lamH ^ 2) * ip G f f := by + have h2c : 2 * (ip G ((zigzag G H e).step f) ((zigzag G H e).step f) + * (1 / (lamG + lamH + lamH ^ 2))) + = 2 * (ip G ((zigzag G H e).step f) ((zigzag G H e).step f) + / (lamG + lamH + lamH ^ 2)) := by + field_simp + rw [h2c] at hc + linarith + rw [div_le_iff₀ hpos] at hkey + show ∑ v : (zigzag G H e).V, ((zigzag G H e).step f v) ^ 2 ≤ _ + rw [sum_sq_eq_ip, sum_sq_eq_ip] + nlinarith [hkey] + +end RegGraph + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/ZigZagBaseExists.lean b/Complexitylib/Classes/PCP/Internal/ZigZagBaseExists.lean new file mode 100644 index 00000000..7cd3c8a2 --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/ZigZagBaseExists.lean @@ -0,0 +1,85 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.ZigZagTower +public import Complexitylib.Classes.PCP.Internal.ExpanderExists + +/-! +# A base graph for the tower exists + +The zig-zag tower is seeded by a single finite graph whose vertices number the +fourth power of its degree and whose spectral bound is a fifth. Such a graph is +what Reingold, Vadhan and Wigderson find by exhaustive search; here it comes +from any expander family at all, by raising one member to a power. + +Powering leaves the vertex set alone and raises both the degree and the bound +to the same power, so taking the member on `degree ^ (4 * m)` vertices and +powering it `m` times gives `degree ^ (4 * m) = (degree ^ m) ^ 4` vertices +against degree `degree ^ m` — and `lam ^ m ≤ 1 / 5` once `m` is large enough. + +Applied to `randExpander`, this shows a `ZigZagBase` exists. + +## Main definitions + +- `Complexity.ExpanderFamily.fifthExp` — a power taking the bound below a fifth +- `Complexity.ExpanderFamily.toZigZagBase` — the base graph + +## Main results + +-/ + +@[expose] public section + +namespace Complexity + +namespace ExpanderFamily + +variable (E : ExpanderFamily) + +theorem exists_pow_le_fifth : ∃ m : ℕ, E.lam ^ m ≤ 1 / 5 := by + obtain ⟨m, hm⟩ := exists_pow_lt_of_lt_one (show (0 : ℝ) < 1 / 5 by norm_num) E.lam_lt_one + exact ⟨m, hm.le⟩ + +/-- A power taking the family's bound to at most a fifth. -/ +noncomputable def fifthExp : ℕ := Classical.choose E.exists_pow_le_fifth + +theorem pow_fifthExp_le : E.lam ^ E.fifthExp ≤ 1 / 5 := + Classical.choose_spec E.exists_pow_le_fifth + +/-- The size at which the powered member has as many vertices as the fourth +power of its degree. -/ +noncomputable def baseOrder : ℕ := E.degree ^ (4 * E.fifthExp) + +/-- **A base for the zig-zag tower.** -/ +noncomputable def toZigZagBase : ZigZagBase where + base := (E.graph E.baseOrder).power E.fifthExp + card_eq := by + have hV : Fintype.card ((E.graph E.baseOrder).power E.fifthExp).V = E.baseOrder := + Fintype.card_fin _ + rw [hV, RegGraph.deg_power, deg_graph, baseOrder, ← pow_mul, Nat.mul_comm] + lam := E.lam ^ E.fifthExp + lam_nonneg := pow_nonneg E.lam_nonneg _ + lam_le := E.pow_fifthExp_le + spectral := RegGraph.spectralBound_power _ (E.spectral_graph _) _ + +theorem one_le_fifthExp : 1 ≤ E.fifthExp := by + by_contra h + have h0 : E.fifthExp = 0 := by omega + have := E.pow_fifthExp_le + rw [h0, pow_zero] at this + norm_num at this + +@[simp] theorem deg_toZigZagBase : E.toZigZagBase.base.deg = E.degree ^ E.fifthExp := by + show ((E.graph E.baseOrder).power E.fifthExp).deg = _ + rw [RegGraph.deg_power, deg_graph] + +theorem one_lt_deg_toZigZagBase (h : 1 < E.degree) : 1 < E.toZigZagBase.base.deg := by + rw [deg_toZigZagBase] + exact Nat.one_lt_pow (by have := E.one_le_fifthExp; omega) h + +end ExpanderFamily + +end Complexity diff --git a/Complexitylib/Classes/PCP/Internal/ZigZagTower.lean b/Complexitylib/Classes/PCP/Internal/ZigZagTower.lean new file mode 100644 index 00000000..eac999af --- /dev/null +++ b/Complexitylib/Classes/PCP/Internal/ZigZagTower.lean @@ -0,0 +1,133 @@ +/- +Copyright (c) 2026 Bolton Bailey. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Bolton Bailey +-/ +module +public import Complexitylib.Classes.PCP.Internal.ZigZag +public import Complexitylib.Classes.PCP.Internal.Power +public import Complexitylib.Classes.PCP.Internal.ExpanderPad + +/-! +# The zig-zag tower + +One graph of constant size generates an infinite family. Square a member — the +degree becomes the fourth power of the base's, exactly the number of the base's +vertices — and zig-zag with the base: the vertex count is multiplied by that +same number while the degree drops back to the base's squared. + +The spectral bookkeeping is what makes the recursion close. If the base has +bound `lam ≤ 1/5` and a member has bound `2/5`, squaring gives `4/25` and the +zig-zag estimate `λ(G ⓩ H) ≤ λ(G) + λ(H) + λ(H)²` gives at most +`4/25 + 1/5 + 1/25 = 2/5`, the invariant again. + +Only the base is non-constructive; the recursion itself is an algorithm, which +is why this yields an expander family a machine can build. + +## Main definitions + +- `Complexity.ZigZagBase` — a constant-size graph to build from +- `Complexity.TowerStep` — a member of the family, with its invariants +- `Complexity.towerSucc` — one round of squaring and zig-zagging + +## Main results + +- `Complexity.towerSucc` carries the invariants forward, by construction +-/ + +@[expose] public section + +namespace Complexity + +/-- The seed of the tower: a graph whose vertices number the fourth power of its +degree, with a spectral bound of at most a fifth. -/ +structure ZigZagBase where + /-- The graph itself. -/ + base : RegGraph + /-- Its vertices number the fourth power of its degree. -/ + card_eq : Fintype.card base.V = base.deg ^ 4 + /-- Its spectral bound. -/ + lam : ℝ + /-- The bound is nonnegative. -/ + lam_nonneg : 0 ≤ lam + /-- And at most a fifth, which is what makes the recursion close. -/ + lam_le : lam ≤ 1 / 5 + /-- The bound holds. -/ + spectral : base.SpectralBound lam + +namespace ZigZagBase + +variable (B : ZigZagBase) + +/-- A member of the family: its degree is the base's squared, and its spectral +bound is two fifths. -/ +structure TowerStep where + /-- The graph. -/ + graph : RegGraph + /-- Its degree is the base's squared. -/ + deg_eq : graph.deg = B.base.deg ^ 2 + /-- Its spectral bound. -/ + spec : graph.SpectralBound (2 / 5) + +/-- Squaring a member makes its degree match the base's vertex count. -/ +theorem card_sq_eq (T : B.TowerStep) : + Fintype.card B.base.V = Fintype.card (T.graph.power 2).D := by + have h1 : Fintype.card (T.graph.power 2).D = (T.graph.power 2).deg := rfl + rw [h1, RegGraph.deg_power, T.deg_eq, B.card_eq] + ring + +/-- So the base's vertices name the squared member's darts. -/ +noncomputable def stepEquiv (T : B.TowerStep) : B.base.V ≃ (T.graph.power 2).D := + Fintype.equivOfCardEq (B.card_sq_eq T) + +/-- **One round of the tower**, against a chosen naming of the base's vertices +by the squared member's darts. The tower itself takes the arbitrary naming +`stepEquiv`; an explicitly encoded tower supplies its own. -/ +noncomputable def towerSuccOf (T : B.TowerStep) (e : B.base.V ≃ (T.graph.power 2).D) : + B.TowerStep where + graph := RegGraph.zigzag (T.graph.power 2) B.base e + deg_eq := by + rw [RegGraph.deg_zigzag] + ring + spec := by + have hsq : (T.graph.power 2).SpectralBound ((2 / 5) ^ 2) := + RegGraph.spectralBound_power T.graph T.spec 2 + have hzz := RegGraph.spectralBound_zigzag (T.graph.power 2) B.base e + hsq B.spectral (by norm_num) B.lam_nonneg + have h5 := B.lam_le + have h0 := B.lam_nonneg + refine hzz.mono ?_ ?_ + · nlinarith [h0] + · nlinarith [h5, h0] + +/-- **One round of the tower.** -/ +noncomputable def towerSucc (T : B.TowerStep) : B.TowerStep := + B.towerSuccOf T (B.stepEquiv T) + +@[simp] theorem graph_towerSucc (T : B.TowerStep) : + (B.towerSucc T).graph = RegGraph.zigzag (T.graph.power 2) B.base (B.stepEquiv T) := rfl + +/-- The first member: the base, squared. -/ +noncomputable def towerZero : B.TowerStep where + graph := B.base.power 2 + deg_eq := by + rw [RegGraph.deg_power] + spec := by + have hsq : (B.base.power 2).SpectralBound (B.lam ^ 2) := + RegGraph.spectralBound_power B.base B.spectral 2 + refine hsq.mono (by positivity) ?_ + have h5 := B.lam_le + have h0 := B.lam_nonneg + nlinarith [h5, h0] + +/-- **The tower.** -/ +noncomputable def tower : ℕ → B.TowerStep + | 0 => towerZero B + | k + 1 => towerSucc B (tower k) + +theorem deg_tower (k : ℕ) : (tower B k).graph.deg = B.base.deg ^ 2 := + (tower B k).deg_eq + +end ZigZagBase + +end Complexity diff --git a/Complexitylib/Encoding/Pairing.lean b/Complexitylib/Encoding/Pairing.lean index 49291008..534f1e42 100644 --- a/Complexitylib/Encoding/Pairing.lean +++ b/Complexitylib/Encoding/Pairing.lean @@ -295,4 +295,14 @@ theorem pairSnd_eq_unpair? (z : List Bool) : unfold pairSnd cases unpair? z <;> rfl + +/-- The second component of a pair is no longer than the pair. -/ +theorem pairSnd_length_le (z : List Bool) : (pairSnd z).length ≤ z.length := by + rcases hu : unpair? z with _ | ⟨p, q⟩ + · rw [show pairSnd z = [] from by rw [pairSnd, hu]] + simp + · have hz : z = pair p q := unpair?_eq_some_iff.mp hu + rw [show pairSnd z = q from by rw [pairSnd, hu], hz, pair_length] + omega + end Complexity diff --git a/Complexitylib/Languages/Contains.lean b/Complexitylib/Languages/Contains.lean index 18dc6191..2e87c9ef 100644 --- a/Complexitylib/Languages/Contains.lean +++ b/Complexitylib/Languages/Contains.lean @@ -53,6 +53,14 @@ def containsZero : Language := {x | ∃ b ∈ x, b = false} /-- Strings containing at least one `1`-bit. -/ def containsOne : Language := {x | ∃ b ∈ x, b = true} +/-- Membership in `containsZero` unfolds to the existence of a `0`-bit. -/ +@[simp] theorem mem_containsZero {x : List Bool} : + x ∈ containsZero ↔ ∃ b ∈ x, b = false := Iff.rfl + +/-- Membership in `containsOne` unfolds to the existence of a `1`-bit. -/ +@[simp] theorem mem_containsOne {x : List Bool} : + x ∈ containsOne ↔ ∃ b ∈ x, b = true := Iff.rfl + end Language -- ════════════════════════════════════════════════════════════════════════