Repository navigation
Order an inequality's endpoints by a comparison that can be made (#757) - #761
Conversation
The roots come out of the quadratic formula as (-b - sqrt(D))/(2a) before
(-b + sqrt(D))/(2a), which is ascending only while a is positive, and
SortRealsAndNonReals orders the ones it can compare and leaves the rest as they
came. Solving expr <= 0 negates the expression, so it arrives with a negative
leading coefficient and the pair the wrong way round: (x - a)(x + a) <= 0 was
answered with an interval running from |a| down to -|a|, which is empty, with the
whole solution set lost between the endpoints.
Ordered by the closed form rather than by a case split on the sign of the symbol.
min(p, q) is (p + q - |p - q|)/2 and max(p, q) is (p + q + |p - q|)/2, so the
answer is the single interval [-|a|; |a|] -- right for either sign of a and for
a = 0 -- rather than three branches each holding on part of the line. That also
keeps the result a Set, which a piecewise is not.
Where both roots are real numbers the sort has already ordered them and they are
handed back untouched, so nothing a concrete coefficient produces changes shape:
(x - 2)(x + 2) <= 0 is still [-2; 2].
A double root now collapses to the point it is. (x - a)^2 <= 0 answers { a } and
< 0 answers the empty set, where both were intervals built from an unevaluated
discriminant.
Simplifying an endpoint may cancel a symbol against itself, which holds only away
from zero and comes back as a Providedf -- and a condition where an endpoint
belongs makes the interval an Entity rather than a Set, which threw
InvalidCastException on a*x^2 - 1 <= 0. The unsimplified endpoint is the same
number and assumes nothing, so it is kept there. Those are the expressions whose
*leading* coefficient is symbolic, which this does not claim to fix: whether the
solution lies inside the roots or outside them turns on the sign of that
coefficient, and that is a second defect, filed separately.
AGENTS.md gains the rule this issue is the worked example for: refusing to answer
is a stopgap and not a fix that works, difficulty is not an argument for choosing
between two approaches, and a closed form is worth looking for before a case
analysis is built.
Measured over 128 specialisations -- solve with the symbol, substitute a value of
each sign, and compare membership against the inequality itself at eleven points:
81/128 correct before and 108/128 after, 27 wrong -> right, 0 right -> wrong, and
master's 8 ElementInSetAmbiguousException cases gone. The 20 that remain are all
the symbolic-leading-coefficient family. Unit tests 5225 -> 5254 passing with 29
added, F# 130, casbench 112/117 with 0 wrong, propcheck 0 failures, rootcheck
596/596, simpsweep 0 disagreements.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…est need SolveInequality.AutoTestSkip was skipped as "Piecewise required" and held six cases. Two of them -- (x + a)(x + b) <= 0 and < 0 -- wanted the smaller of two symbolic roots as the left endpoint, which the previous commit gives a closed form for, so no case split was needed to answer them after all. They move to AutoTest and pass. The remaining four put a symbol on the right-hand side, which moves the roots rather than permuting them: whether the quadratic has real roots at all turns on the sign of a + 1/4, and where it has none the answer is the whole line or nothing, which is no interval between endpoints in any order. The skip reason now says that instead of "Piecewise required", and points at #762 for the sibling case of a symbolic leading coefficient. Unit tests 5254 -> 5256 passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Follow-up commit: The other four put a symbol on the right-hand side, which moves the roots rather than permuting them: whether the quadratic has real roots at all turns on the sign of Unit tests 5254 → 5256. |
Closes #757.
What was wrong
The quadratic formula returns
(-b - sqrt(D))/(2a)before(-b + sqrt(D))/(2a), which is ascending only whileais positive, andSortRealsAndNonRealsorders the roots it can compare and leaves the rest as they came. Solvingexpr <= 0negates the expression, so it arrives with a negative leading coefficient and the pair the wrong way round:an interval running from
|a|down to-|a|— empty for either sign, with the entire solution set lost between the endpoints.The fix, and why it is one interval and not three branches
The issue proposed a
Piecewisecase split on the sign ofa. It does not need one: the ordering has a closed form.min(p, q)is(p + q - |p - q|)/2andmax(p, q)is(p + q + |p - q|)/2, so the answer is the single interval[-|a|; |a|]— the issue's own first line — right for either sign ofaand fora = 0.That is better than the case split on three counts: it is one interval rather than three branches each holding on part of the line; it keeps the result a
Set, which aPiecewiseis not (it is anEntity, soSolvecannot return one); and it collapses to exactly the old output when the roots are concrete, so(x - 2)(x + 2) <= 0is still[-2; 2]and nothing a numeric coefficient produces changes shape.Where both roots are real numbers the existing sort has already ordered them and they are handed back untouched.
A double root now collapses to the point it is
Not sought, but it falls out — the span between coincident endpoints is empty:
One thing this does not fix, and a crash it did cause
Simplifying an endpoint can cancel a symbol against itself, which holds only away from zero and comes back as a
Providedf. A condition where an endpoint belongs makes the interval anEntityrather than aSet— that threwInvalidCastExceptionona*x^2 - 1 <= 0in the first version of this change. The unsimplified endpoint is the same number and assumes nothing, so it is kept there.Those are exactly the expressions whose leading coefficient is symbolic, and this PR does not claim to answer them. Whether the solution lies inside the roots or outside them turns on the sign of that coefficient, and unlike the endpoint ordering that has no closed form — it genuinely needs the case split. It is a second defect, filed separately with the evidence below.
Measured
Correctness here cannot be read off the printed form (
1/2 * abs(X)andabs(X) / 2are one number and two trees), so each case is checked by membership: solve with the symbol, substitute a value of each sign, and compare what the resulting set holds at eleven points against the inequality itself.128 specialisations — 8 shapes × 4 directions ×
a ∈ {3, -3, 1/2, -1/2}:ElementInSetAmbiguousException27 wrong → right, 0 right → wrong. Every one of the 20 that remain is the symbolic-leading-coefficient family above.
Suites and harnesses, from a clean build:
casbench112/117 solved, 0 wrong, 0 error, 0 timeout — unchangedpropcheck0 failures,rootcheck596/596,simpsweep0 disagreements of 10463AGENTS.md
Also carries the rule this issue turned out to be the worked example for: "not answering is a legitimate answer" is a floor and not a target, difficulty is not an argument for choosing between two approaches, and a closed form is worth looking for before a case analysis is built. It sits under the existing section it qualifies, since that is the rule that is easiest to misread as a licence to refuse.