Skip to content

Triangularise a polynomial system instead of eliminating it in radicals - #865

Open
Rafael-SOWNet wants to merge 3 commits into
masterfrom
feature/groebner-system-solver
Open

Triangularise a polynomial system instead of eliminating it in radicals#865
Rafael-SOWNet wants to merge 3 commits into
masterfrom
feature/groebner-system-solver

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

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, size compounds:

polynomial result size
x⁴ - 10x³ + 35x² - 50x + 24 (numeric coefficients) 5 nodes
x⁴ + ax³ + bx² + cx + d (symbolic coefficients) 5661 nodes

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

before after
4 coupled variables no answer in 300 s 24 solutions, 23 ms
5 coupled variables over 60 s 120 solutions, 129 ms
over-determined, consistent (3 eq, 2 var) WrongNumberOfArgumentsException 2 solutions, 1 ms
over-determined, inconsistent WrongNumberOfArgumentsException no solutions, 0 ms
everything else unchanged

A Gröbner 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 — 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:

  • Coefficient width. A system was seen to run away with 53 pairs, 64 terms and 188-digit rationals. No count of pairs, basis size or terms would have caught it — the term count never grew.
  • Quotient dimension, checked before FGLM starts rather than discovered inside it. FGLM's cost is governed by the number of solutions, which is a different axis from anything bounding Buchberger: a system can have a basis that computes in milliseconds and a conversion that never finishes.

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

MultivariatePolynomial becomes partial (one word) so the operations only this needs live in Functions/Algebra/Groebner beside their consumer, instead of swelling the type simplification uses. Moving the rest of the polynomial kernel out of Functions/Simplification is 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.md with 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

  • 6079 C# tests pass, 0 failed — 12 new, and the suite is at its usual runtime (4 m 37 s)
  • 130 F# wrapper tests pass
  • the existing SolveSystem tests, which re-substitute every returned root numerically, are unchanged and green

Not in this PR

  • Multi-modular arithmetic, which would lift the remaining ceiling by attacking coefficient swell directly
  • Algebraic numbers, which are what would let this answer systems whose solutions are not rational — the keystone, and much larger
  • The polynomial-kernel file move

🤖 Generated with Claude Code

Rafael-SOWNet and others added 3 commits August 10, 2026 03:02
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>
@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator Author

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 Simplify, which searches — it generates candidate forms and picks between them, so how long it takes to settle a nested radical is bounded by nothing. Restricting the input avoided that. Bounding the check is better.

InnerSimplified is a single structural pass, and it turns out to be enough:

identity proved zero in
sqrt(2)^2 - 2 13 ms
(3^(1/3))^3 - 3 13 ms
((1+sqrt(5))/2)^2 - (1+sqrt(5))/2 - 1 3 ms
sqrt(3 + 2*sqrt(2)) - 1 - sqrt(2) 3 ms
a Cardano cube root of unity 15 ms

So the gate is gone and radicals are in reach:

system result
x^2 - 2, y - x (sqrt(2), sqrt(2)), (-sqrt(2), -sqrt(2)) — 57 ms
x^2 - 2, y^2 - 3 4 exact solutions — 11 ms
x^3 - 2, y - x 3 solutions incl. complex cube roots — 104 ms

The property that makes it safe is unchanged: InnerSimplified only ever proves zero, never disproves it, so a candidate it cannot settle is declined and the system falls back. Coverage, never correctness — and still no tolerance anywhere.

One existing test moved out of the fallback and into this path, getting faster and exact at once: TestPolySystem3 went from 629 ms of numeric answers to 22 ms of radicals. The degree-nine system stays out — decimal roots leave nothing to prove an identity with — and now costs 941 ms rather than 282. Bounded, and worth the rest.

6102 C# and 130 F# tests pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant