feat(univariate): Cantor-Zassenhaus linear-factor splitter and root finder #252
feat(univariate): Cantor-Zassenhaus linear-factor splitter and root finder #252DimitriosMitsios wants to merge 21 commits into
Conversation
… + soundness) Add an executable Cantor–Zassenhaus equal-degree (degree-one) splitter for field-root products over odd-cardinality finite fields, plugging into the `Univariate/Roots` `LinearFactorProductSplitter` interface alongside the smooth-subgroup splitter (PR Verified-zkEVM#244). - czRefine / czSplitWithShifts / czSplitLinearFactors: executable algorithm. - czSound: every emitted factor is linear (proved). - czShift_eq_linearFactor: the shift bucket `X + s` is `linearFactor (-s)`, the first link of completeness (prime-field isolation, no QR needed). - czLinearFactorProductSplitterOf: adapter taking `complete` as a parameter. Completeness (prime fields) is in progress on this branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Analytic core of the quadratic-residue routing: at a root a of p, shiftedPowModWith M D p s k evaluates to (a+s)^k, wrapping the framework's raw_eval_powModWith_eq_pow. Plus eval_X_add_C. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
czRefine_root_in_residue_bucket: for odd q and a+s != 0, a root a of p lands in one of the residue buckets gRes/gNon. Uses Fermat (a^q=a) to show the discriminating power (a+s)^((q-1)/2) squares to 1, hence is +/-1 at a. Adds eval_add helper and czRefine projection lemmas. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gcdMonic_linearFactor_of_root: at a root a of p, gcdMonic p (linearFactor a) = linearFactor a, via toPoly + normalize_eq_normalize + Monic.normalize_eq_self. This makes the s=-a quotient bucket of czRefine isolate the root. Adds linearFactor_toPoly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
monicNormalize fixes the monic linear factor X - a, completing the s=-a isolation chain (gZero = linearFactor a). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
czSplit_emits: a normalized represented linear factor with root a is emitted as a candidate. Plus represented_coeff_one_ne_zero, represented_zero_one_eq_false. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
czComplete_core: the recursion finds a root factor candidate for every root a (q odd, schedule reaches -a). Root preserved into a residue bucket at each non-isolating shift (QR routing), isolated at s=-a via the X+s bucket. czComplete: top-level completeness given prime-field schedule coverage. Adds eval_linearFactor_self and czSplitWithShifts_cons_else. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Concrete LinearFactorProductSplitter from Cantor-Zassenhaus, with validInput recording odd q, Frobenius, and prime-field schedule coverage. Completeness discharged by czComplete. The CZ splitter is now sound and complete over odd prime fields, with no sorry/native_decide. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…zmod) Discharges czComplete's validInput for ZMod q (odd prime) via ZMod.pow_card and schedule coverage (zmod_mem_czDefaultShifts). Covers KoalaBear/BabyBear. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…od 7 #guard checks that czSplitLinearFactors splits (X-2)(X-3) and X(X-1) into linear factors (including root 0), all factors linear, plus a czComplete_zmod instantiation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update module doc to state soundness/completeness are proved over odd prime fields and note the schedule-length efficiency caveat; trim over-explanation and an em-dash connective. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…actor Introduce HasRootFactor abbreviation for the repeated 'exists a linear root factor candidate' conclusion, removing the deeply nested existential from czComplete/_core/_zmod, czSplit_emits, and the splitter builder parameter. Add HasRootFactor.append_left/right to collapse the three membership lifts in czComplete_core to one line each. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the Cantor-Zassenhaus splitter into the framework root pipeline (rootsInFiniteField): czFiniteFieldContext for ZMod q plus czRoots, with czRoots_sound/czRoots_complete derived from the orchestrator's correctness and the splitter's validInput facts. Finds the distinct field roots of an arbitrary univariate f (stage 1 gcd(f, X^q - X) + CZ split). Tests over ZMod 7 incl. multiplicity collapse and a rootless irreducible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Trim proof sketches and usage motivation from 11 declaration docstrings (shiftedPowModWith, czShift_eq_linearFactor, eval_shiftedPowModWith, eval_add, czRefine_root_in_residue_bucket, gcdMonic_linearFactor_of_root, czSplitWithShifts, czComplete_core, zmod_mem_czDefaultShifts, czComplete_zmod); drop inaccurate 'odd order' from czFiniteFieldContext. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Trim the 'why abstract hypotheses' and 'stage 1 then splitter' how-detail from two section headers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reword the module-doc efficiency note so the fast large-field (short/random schedule) variant and its QR-separation completeness are clearly marked as unimplemented future work, not an existing guarantee. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🤖 PR SummaryMathematical Formalization
Proof Completion (sorries removed)
Infrastructure / CI
Documentation
Key architectural notes
Headers used
Statistics
Lean Declarations ✏️ Added: 41 declaration(s)
📋 **Additional Analysis**The patch adds a new file implementing the Cantor–Zassenhaus linear-factor splitter for odd prime fields, along with a test file and updates to the import tree. The implementation is well-structured, all definitions and theorems have docstrings, and the code generally follows the project’s naming and style conventions. However, a few items deviate from the supplied contributing guidelines. 📄 **Per-File Summaries**
Last updated: 2026-07-14 00:03 UTC. |
dhsorens
left a comment
There was a problem hiding this comment.
hey @DimitriosMitsios ! I'm happy with this PR. It's concise, and gets the foundational theory there. I think this is a good start to something like #254 which really digs into all the randomization theory.
Just a note on the style guide, if you could make those updates we can go ahead and get this merged in.
| /-- One Cantor–Zassenhaus refinement against shift `s`: returns the three | ||
| sub-factors `(gcd(p, X+s), gcd(p, w-1), gcd(p, w+1))` where `w` is the shifted | ||
| discriminating power. Their product is `p` (for squarefree `p` of odd `q`). -/ | ||
| def czRefine (M : CPolynomial.Raw.MulContext F) (D : CPolynomial.Raw.ModContext F) |
There was a problem hiding this comment.
note on style (from the review bot):
- Theorem Naming: Most theorems in CompPoly/Univariate/Roots/CantorZassenhaus.lean use mixed camelCase_snake_case (e.g., czShift_eq_linearFactor, czRefine_fst, czComplete_zmod). According to the style guide, theorems and proofs should use snake_case (e.g., cz_shift_eq_linear_factor, cz_refine_fst, cz_complete_zmod).
- Function Syntax: Several functions and proofs use fun x => ... (lines 222, 319, 368, 390). The guide prefers the maps-to arrow: fun x ↦ ....
There was a problem hiding this comment.
@dhsorens I went through the changes:
- Theorem naming: I think that the bot is confusing snake_case as "lowercase every component". I suggest that we keep names as they are because the same convention is found in many other places e.g.:
- Univariate/Basic.lean: natDegree_C, coeff_divX, divX_size_lt
- Univariate/EuclideanAlgorithm.lean: xgcdAux_bezout, monicNormalize_toPoly_eq_normalize
- Univariate/DivisionCorrectness.lean: divByMonic_toPoly_eq_divByMonic, leadingCoeff_inv_smul_monic
- Univariate/ToPoly/: toPoly_add, toPoly_mul_coeffC
- Function syntax: Fixed
Per the style guide and matching the neighboring SmoothSubgroup code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DimitriosMitsios is this implementation with |
|
@olympichek Yes they are effectively the same because the current PR implements a deterministic version of CZ. They are not the same algorithms though. If the randomized CZ is built on top of the current PR then they would diverge. |
alexanderlhicks
left a comment
There was a problem hiding this comment.
Full review pass: I read the proofs line by line and the CZ module also compiles cleanly for me locally. The math is sound — czSound and czComplete_core check out (including the subtle detail that the [] case of czSplitWithShifts still emits represented linear factors, which the a + s = 0 branch of completeness relies on), no sorry/native_decide/custom axioms, and the schedule-parametric design is the right foundation for randomized CZ (#254).
Two things before merge:
- master has moved 4 commits ahead, including the Lean/mathlib v4.31.0 bump (#264) — please merge master and let CI re-run on the new toolchain.
- The small inline points below (includes one leftover from the earlier
fun ↦style pass).
|
|
||
| /-! ### Every emitted factor is linear (soundness, on a concrete input). -/ | ||
|
|
||
| #guard (czSplitLinearFactors 7 p₂₃).all (fun f => isRepresentedLinearFactor f) |
There was a problem hiding this comment.
Leftover from the fun ↦ style pass: this one still uses =>. Could be fun f ↦ isRepresentedLinearFactor f, or simply .all isRepresentedLinearFactor.
| q (czDefaultShifts q) p | ||
|
|
||
| /-- Soundness: every factor emitted by the CZ schedule is a linear factor. -/ | ||
| theorem czSound (q : Nat) (shifts : List F) (p factor : CPolynomial F) |
There was a problem hiding this comment.
Naming: the def-referencing camelCase names elsewhere in this file (czRefine_fst, linearFactor_toPoly, …) match repo/Mathlib practice and are fine as-is, but czSound, czComplete, czComplete_core, czComplete_zmod, and czSplit_emits don't reference any def and diverge from the repo's <def>_property pattern (rootsInFiniteField_sound, enumeratedLinearFactors_complete, …). Suggest:
czSound→czSplitWithShifts_soundczSplit_emits→czSplitWithShifts_emitsczComplete_core→czSplitWithShifts_completeczComplete→czSplitLinearFactors_completeczComplete_zmod→czSplitLinearFactors_complete_zmod
| rw [eval_X_add_C] | ||
|
|
||
| /-- Evaluation is additive. -/ | ||
| theorem eval_add (a : F) (p₁ p₂ : CPolynomial F) : |
There was a problem hiding this comment.
eval_add (and arguably eval_X_add_C above) is a fully generic CPolynomial lemma, not CZ-specific. eval_sub and eval_one already live in CompPoly/Univariate/ToPoly/Impl.lean — this belongs next to them, and CompPoly/Univariate/CoefficientInterpolation.lean has a private eval_add that could then be dropped in favor of the shared one.
|
|
||
| /-- One Cantor–Zassenhaus refinement against shift `s`: returns the three | ||
| sub-factors `(gcd(p, X+s), gcd(p, w-1), gcd(p, w+1))` where `w` is the shifted | ||
| discriminating power. Their product is `p` (for squarefree `p` of odd `q`). -/ |
There was a problem hiding this comment.
Docstring nit: “Their product is p (for squarefree p of odd q)” is not proved anywhere in the file, and as stated it's slightly imprecise — p must be a squarefree product of linear factors (as produced by stage 1), not merely squarefree. Since nothing depends on the claim, suggest rewording (e.g. “for a squarefree product of linear factors, the three parts multiply back to p”) or dropping it.
| The default schedule has length `q`, so it is practical only on small fields. A | ||
| fast large-field variant would use a short (e.g. random) schedule; proving its | ||
| completeness needs the quadratic-residue separation argument and is left as future | ||
| work. |
There was a problem hiding this comment.
Following up on the earlier question from @olympichek about enumeratingLinearFactorProductSplitter: it would help future readers to state the contrast here in the module docstring. Proof-guarantee-wise, czComplete_zmod currently duplicates what enumeration already provides for ZMod q — but computationally they differ: enumeration always pays Θ(q·deg) evaluations, while czSplitWithShifts stops consuming the schedule as soon as every bucket is linear (typically a handful of shifts × O(log q) mul-mods), and the schedule-parametric proofs are the base the randomized variant (#254) can build on, which enumeration can never become. One or two sentences on “when to use this vs Enumeration.lean” would preempt the question.
| #guard (czRoots 7 fMult).contains 3 | ||
|
|
||
| -- `X² + 1` is irreducible over `ZMod 7` (`-1` is a non-residue), so no roots. | ||
| #guard (czRoots 7 (CPolynomial.X ^ 2 + 1)).size == 0 |
There was a problem hiding this comment.
Coverage suggestion: consider adding degenerate-input guards, e.g.
#guard (czRoots 7 (0 : CPolynomial F)).size == 0
#guard (czRoots 7 (CPolynomial.C 5)).size == 0(the backend handles these via its size guards; nice to pin that behavior).
Claude exaggerated my level of detail here, but at least they build. 😄 |
This PR implements the Cantor–Zassenhaus root-finding algorithm over odd prime fields. It is implemented in two stages:
Univariate/Rootspipeline.The splitter is proved sound and complete over odd prime fields (no sorry, no native_decide).
The original paper includes a probabilistic choice of shift parameters. On the other hand, the current implementation goes through all field elements deterministically (the schedule 0..q−1), which guarantees completeness but is practical only on small fields. The shift schedule is included as an explicit argument in the splitter and its proofs, so correctness does not depend on how the shifts are chosen — swapping in random shifts is just a different schedule.