Give the resultant a consumer, and publish the polynomial layer (#746 item 43) - #1017
Conversation
The inequality solver refused everything above degree two. It now builds a sign table: a polynomial has one sign on each open interval between consecutive real roots, so the answer is the union of the intervals where that sign is positive. What makes it an answer rather than a guess is that the root list is complete. The polynomial is written as a product of powers of irreducibles over Q -- verified to multiply back -- and the number of real roots of each irreducible factor is read off its discriminant: two where a quadratic factor's is positive and none where it is negative, three and one respectively for a cubic. Four is where the discriminant stops deciding, so an irreducible factor of degree four or more is refused. This is the first production caller of PolynomialResultant, which had none outside its own tests. The ordering of the roots is decided numerically and therefore checked rather than trusted: the sign of the polynomial at an exact rational point in each interval is computed in exact arithmetic, and the sequence has to change sign at every root of odd multiplicity and hold it at every root of even multiplicity. The outermost two samples sit beyond Cauchy's bound. Three cases in KnownLimitsAreNotBugsTest that pinned the old refusal now answer, and move to the theory for the ones that do; the ones left there are irreducible quartics, which is what the remaining gap actually is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011emtnRT6EWTrxXNtqDVK3e
Factorisation over Q, multivariate greatest common divisors, resultants, discriminants and square-free parts are all in the kernel assembly already and are all called by Simplify, Solve and Integrate. None of them could be asked for directly, so a caller who wanted the factors rather than whatever a simplification decided to do with them had no way to say so -- and no way to find out that the request was outside what the layer can do. Five members, each documented with a worked example that is executed by PolynomialSurfaceTest, so the page cannot drift from the output. Every one refuses with null rather than guessing, and null means "I could not settle this" and never "the answer is the input". Factor hands back an irreducible polynomial unchanged, which is the statement that it is irreducible; it hands back null for a multivariate one, because returning x * y + y from a factorisation would say that y * (x + 1) does not exist. Nine variables, degree 128 and degree 33 are refused the same way. Telling those two apart needed a change inside PolynomialFactorization: Factor reported an irreducible polynomial as null, since its callers are looking for a product to work through, and that is the same value it uses for declining. FactorComplete is the entry point that separates them, and Factor is now the same computation with the trivial factorisation rejected. PublicApi.txt regenerated: five members and one type added, none removed. The growth figure in Package.Build.props counts the same lines and moves with them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011emtnRT6EWTrxXNtqDVK3e
The discriminant decides how many real roots an irreducible factor has up to degree three. At four it decides half of it -- negative means exactly two real roots -- and P = 8ac - 3b^2 with D = 64a^3e - 16a^2c^2 + 16ab^2c - 16a^2bd - 3b^4 decide the other half: four real where both are negative, none where either is positive. Where neither holds the criterion says nothing, and neither does this. So x^4 + 1 > 0 is the whole line, x^4 - 2 > 0 is the outside of a pair of fourth roots, and x^4 - 10x^2 + 1 > 0 has three intervals. The gap is now an irreducible factor of degree five or more, where there is no criterion and no formula for the roots either. The selection of the real roots among the complex ones generalises from the cubic's one-of-three to any count below the degree, with the same guard: the two groups have to be far enough apart for saying which is which not to be a close call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011emtnRT6EWTrxXNtqDVK3e
The sample points of the sign table are placed from the double approximations of the roots. A rational root whose numerator dwarfs its denominator by more than a double can express comes back as an infinity, and the midpoints computed from it are nowhere in particular. It is refused now, which is the answer that shape of input has. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011emtnRT6EWTrxXNtqDVK3e
|
One commit landed after the description above was written, and it belongs in it — The sign table places its sample points from double approximations of the roots, then evaluates the polynomial there in exact arithmetic. A rational root whose numerator dwarfs its denominator by more than a It is refused now. That keeps the property the rest of the change rests on: every interval endpoint in an answer is one the sign sequence was actually verified across. A sign table built on sample points that drifted is not a weaker answer, it is a wrong one — a missed or misplaced root merges two intervals of opposite sign and reports the wrong half as the solution. Worth noting because it is the shape of thing this whole change is careful about: the numeric part is used only to order the roots and is checked against exact arithmetic afterwards, and where the numeric part cannot be trusted at all the answer is no answer. |
The entry for polynomial inequalities of degree three and up sits under '2.3.0 -- since 2.2.0'. It merged as #1017 today, after v2.3.0 was tagged, and git show v2.3.0:BREAKING-CHANGES.md does not contain it -- so the file tells a reader the change shipped in a release that does not have it, which is the one thing this file exists to get right. Moved verbatim to Unreleased, both the at-a-glance row and the prose section. Sorting the whole file before and after gives an identical multiset of lines: nothing is added, reworded or lost.
Two halves of #746 item 43, and the first is the one that matters.
(a) The resultant had no caller, and now it has one that needs it
PolynomialResultant— 307 lines with a careful content-extraction argument and a measured work ceiling — was referenced nowhere outside its own file except its unit test, ten days after landing flagged as "on probation until something eliminates a variable with them". Every other file in the layer is wired:PolynomialGcd→PatternsandRationalFunction,PolynomialFactorization→AnalyticalEquationSolverandPartialFractions,PartialFractions→IndefiniteIntegralSolver,RationalFunction→Transformation.Catalogue. Under this issue's own counterweight — infrastructure must be validated by a consumer in the same change — that made it speculative code.The consumer is polynomial inequalities of degree three and up, which previously threw.
AnalyticalInequalitySolverhandled linear and quadratic only.A polynomial has one sign on each open interval between consecutive real roots, so the answer is the union of the intervals where the sign is right — provided the root list is complete. A missed root merges two intervals of opposite sign and reports the wrong half as the solution, which is a wrong answer, not an incomplete one. So completeness has to be established rather than hoped for, and that is what the discriminant is for here:
PolynomialFactorization.FactorPrimitivewrites the polynomial as a product of powers of irreducibles over ℚ, and verifies the product.PolynomialResultant.Discriminant— two or none for a quadratic by its sign; three or one for a cubic; and for a quartic the discriminant together withP = 8ac − 3b²andD = 64a³e − 16a²c² + 16ab²c − 16a²bd − 3b⁴.Degree five is where it stops, and honestly: an irreducible quintic factor has no real-root criterion and no radical formula either.
Measured on builds of each side, and re-verified independently on this machine:
Checked by 32 point-verified theory cases — 121 rational points each, membership against
EvalBoolean— not by printed form.The suggestion I rejected, with the reason.
Discriminantfor repeated-root detection inAnalyticalEquationSolverwould be decorative: that solver already runsTryFactorIntoIrreducibles, whoseSquareFreeDecompositioncomputesgcd(f, f′)— which detects and divides out multiplicity at any degree, where the discriminant only answers yes/no and only up to the resultant's size budget. The sign table is where the discriminant does work nothing else in the tree does: it is the completeness certificate for a real-root list, and without it the interval endpoints would be a guess.Rothstein–Trager (item 44) is therefore no longer the argument for keeping the resultant.
(b)
MathS.PolynomialsFactor,Gcd,Resultant,Discriminant,SquareFreePart— five members, each with XML documentation and a worked example.Does it belong in the kernel? Yes, and the argument is that it adds nothing to it. Every one is a thin adapter over code already compiled into
AngouriMath.dlland already reached bySimplify,SolveandIntegrate. No new machinery ships, the common case pays exactly what it paid before, and the trimmer's picture is unchanged becauseSimplify/Solvealready reference the layer. Item 78 says a large thing landing in the kernel wants the package decision first (now #1008); this lands nothing.What it does cost is a promise for the rest of 2.x, and that is bought with this issue's own complaint: the layer's genuine limits were previously invisible, showing up as a simplification that quietly did nothing.
Refusals are honest —
nullmeans "I could not settle this", never "the answer is the input":Telling irreducible from declined needed one change inside the layer:
PolynomialFactorization.Factorreturnednullfor both.FactorCompletenow separates them;Factoris the same computation with the trivial factorisation rejected, and its existing callers are unchanged.Measured
Suite: 7615 passed, 0 failed, 14 skipped (baseline 7533/0/14) — +82, being 32 inequality cases, 44 surface cases and 6 that moved out of the "not supported" theory. F# wrapper 134/0.
Three tests in
KnownLimitsAreNotBugsTestpinned the old refusal and now answer. They moved toTheDegreesThatAreSupportedStillAnswer, and the refusal theory now lists irreducible quintics, which is what the gap actually is.No regression on solving. Both arms in one session, back to back:
Read the allocation column, not the time column. Byte-identical to within 0.005% on all five — the solving path does the same work, which is the actual answer to "did this regress solving". The timings on that pair moved by up to 9% in both directions with five to eight other agents building on the same 8 cores;
ParseEasy,SimplifyEasyandEvalEasymoved by more than anySolverow with identical allocation, which no change here could cause. Nothing is claimed as a speed-up.PublicApi.txtregenerated: +5 members, +1 type, 0 removals.Sources/Package.Build.propscarried a figure of 103 additions since the 2.2.0 baseline that counted those same lines; it was true and this change falsified it, so it now reads 109.Not done, and why
Entity.Factorize()still does not use the polynomial layer.x^4 - 5x^2 + 4comes back unchanged from it whileMathS.Polynomials.Factorsplits it into four linear factors. That is a real inconsistency and it lives inCore/Transformations/**, which another branch owned during this work — worth its own issue.PartialFractionson the public surface — it needs a public type for the decomposition, which is a larger promise than five scalar-returning members.