Triangularise a polynomial system instead of eliminating it in radicals - #865
Triangularise a polynomial system instead of eliminating it in radicals#865Rafael-SOWNet wants to merge 3 commits into
Conversation
Towards #860. EquationSystem.Solve eliminated one variable at a time by calling SolveEquation, which applies the closed-form radical formulas. With numeric coefficients those are cheap; with symbolic ones they are not, and since each elimination turns the next one's coefficients into nested radicals the size compounds -- the same quartic is 5 nodes with numeric coefficients and 5661 with symbolic ones. Four coupled variables did not finish in 300s while four uncoupled ones with 256 solutions took 17ms, so the cost was never the size of the system. 4 coupled variables no answer in 300 s -> 24 solutions, 23 ms 5 coupled variables over 60 s -> 120 solutions, 129 ms A Groebner basis eliminates without radicals: the lexicographic basis is triangular and leaves the last variable a univariate with rational coefficients, which PolynomialSolver already handles. The basis is computed under degree-reverse-lexicographic, which is the order that can be computed at all -- lexicographic dies at five variables on dense input from coefficient swell -- and converted by FGLM. Bounded, because Buchberger is doubly exponential in the worst case. Two of the four ceilings exist because measurement put them there rather than theory: coefficient width, after a system was seen to run away with 53 pairs and 64 terms and 188-digit rationals, which no count of pairs or terms would have caught; and quotient dimension, checked before FGLM starts rather than discovered inside it, because a system can have a basis that computes in milliseconds and a conversion that never finishes. The path is deliberately narrow: it answers only where every solution is rational. The first cut verified candidates with a full Simplify, which is unbounded work outside every budget -- on a degree-nine univariate it spent longer failing to prove a nested radical satisfied the system than the old solver takes to solve the whole thing, and hung the suite. With rational coordinates the check is exact arithmetic and immediate. So this takes the systems that were hanging and leaves the rest exactly as they were, including the equation-count refusal. Over-determined systems are solved rather than refused, and an inconsistent one reports itself as having no solutions instead of throwing. Both are breaking and recorded. Fewer equations than unknowns still throws: that ideal is not zero-dimensional. MultivariatePolynomial becomes partial so the operations only this needs live in Functions/Algebra/Groebner beside their consumer. Moving the rest of the polynomial kernel out of Functions/Simplification is a separate, mechanical change. Verified: 6079 C# tests and 130 F# tests pass, with the suite at its usual runtime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…stem-solver # Conflicts: # BREAKING-CHANGES.md # Sources/.editorconfig
… search
The restriction to rational solutions was the right fix for the wrong reason. What hung
the suite was verifying candidates with Simplify, which searches -- it generates
candidate forms and picks between them, so how long it takes to settle a nested radical
is not bounded by anything. Restricting the input was one way to avoid that. Bounding the
check is a better one.
InnerSimplified is a single structural pass, and it is enough:
sqrt(2)^2 - 2 zero in 13 ms
(3^(1/3))^3 - 3 zero in 13 ms
((1+sqrt(5))/2)^2 - (1+sqrt(5))/2-1 zero in 3 ms
sqrt(3 + 2*sqrt(2)) - 1 - sqrt(2) zero in 3 ms
a Cardano cube root of unity zero in 15 ms
So the rational-only gate is gone and radical solutions are in reach:
x^2 - 2, y - x -> (sqrt(2), sqrt(2)), (-sqrt(2), -sqrt(2)) 57 ms
x^2 - 2, y^2 - 3 -> 4 solutions, all exact 11 ms
x^3 - 2, y - x -> 3 solutions, including complex cube roots 104 ms
It reads as one pass proving what it can rather than as a narrower input class, which is
also what it actually is: InnerSimplified only ever proves zero and never disproves it, so
a candidate it cannot settle is declined and the system falls back. That costs coverage,
never correctness, and no tolerance is involved at any point.
One existing test moves from the fallback into this path and gets faster and exact with
it: the system with a 0.1 coefficient went from 629 ms of numeric answers to 22 ms of
radicals. The degree-nine system stays out, since decimal roots leave nothing to prove an
identity with, and now costs 941 ms rather than 282 -- bounded, and worth the rest.
Verified: 6102 C# tests and 130 F# tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed a follow-up that widens this past rational solutions. The rational-only restriction was the right fix for the wrong reason. What hung the suite was verifying candidates with
So the gate is gone and radicals are in reach:
The property that makes it safe is unchanged: One existing test moved out of the fallback and into this path, getting faster and exact at once: 6102 C# and 130 F# tests pass. |
Towards #860.
EquationSystem.Solveeliminated one variable at a time by callingSolveEquation, which applies the closed-form radical formulas. With numeric coefficients those are cheap; with symbolic ones they are not, and since each elimination turns the next one's coefficients into nested radicals, size compounds:x⁴ - 10x³ + 35x² - 50x + 24(numeric coefficients)x⁴ + ax³ + bx² + cx + d(symbolic coefficients)Four coupled variables did not finish in 300 s, while four uncoupled ones producing 256 solutions took 17 ms. The cost was never the size of the system — it was eliminating in radicals.
What it does now
WrongNumberOfArgumentsExceptionWrongNumberOfArgumentsExceptionA Gröbner basis eliminates without radicals: the lexicographic basis is triangular and leaves the last variable a univariate with rational coefficients, which
PolynomialSolveralready handles. The basis is computed under degree-reverse-lexicographic — the order that can be computed at all; lexicographic dies at five variables on dense input from coefficient swell — and converted by FGLM.Bounded, and two of the bounds came from measurement
Buchberger is doubly exponential in the worst case, so declining has to be reachable. Four ceilings, and two of them exist because a measurement put them there rather than theory:
Deliberately narrow: rational solutions only
This is the part worth reviewing hardest, because my first cut got it wrong.
It verified each candidate with a full
Simplify()— unbounded work outside every budget. On a degree-nine univariate (x³ + 9x²y - 10,y³ + xy² - 2, an existing test) it spent longer failing to prove a nested radical satisfied the system than the old solver takes to solve the whole thing, and hung the suite.The path now takes only systems where every solution is rational. Then verification is exact rational arithmetic and immediate. That is not a workaround for the bug — it is the honest edge of what can be checked cheaply, and it covers exactly the case that was hanging. That test now declines in 282 ms and passes via the existing solver.
Verification is still done, and matters: a triangular basis can hand back a tuple satisfying the triangle but not the system it came from, where the ideal is not in shape position. No tolerance is involved anywhere — a tolerance is what turns a root that is merely close into one that is reported.
Placement
MultivariatePolynomialbecomespartial(one word) so the operations only this needs live inFunctions/Algebra/Groebnerbeside their consumer, instead of swelling the type simplification uses. Moving the rest of the polynomial kernel out ofFunctions/Simplificationis a separate, mechanical change and is not in this PR.Breaking
Over-determined systems being solved, and inconsistent ones reporting no solutions instead of throwing, are both behaviour changes — recorded in
BREAKING-CHANGES.mdwith the migration. Fewer equations than unknowns still throws: that ideal is not zero-dimensional, so there is no finite set of solutions to enumerate.Verification
SolveSystemtests, which re-substitute every returned root numerically, are unchanged and greenNot in this PR
🤖 Generated with Claude Code