Skip to content

Turn a negated difference round wherever it sits (#882) - #905

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
fix/simplify-descends
Aug 12, 2026
Merged

Turn a negated difference round wherever it sits (#882)#905
Rafael-SOWNet merged 1 commit into
masterfrom
fix/simplify-descends

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

Closes #882.

-(a - b) became b - a for a whole expression and not for the same expression inside another node,
so what a caller got depended on where the subexpression sat:

input was is
-(5 - sqrt(-11)) sqrt(-11) - 5 unchanged
-(5 - sqrt(-11)) + y y - (5 - sqrt(-11)) sqrt(-11) - 5 + y
2 ^ (-(5 - sqrt(-11))) left as written 2 ^ (sqrt(-11) - 5)
sgn(-(5 - sqrt(-11))) left as written sgn(sqrt(-11) - 5)
[[-(5 - sqrt(-11)), 1]] left as written [[sqrt(-11) - 5, 1]]
Expand([[(x+1)^2, 1]]) [[(x + 1) ^ 2, 1]] [[1 + 2 * x + x ^ 2, 1]]

Every one of these was already the right number, written the long way round.

Why it only worked at the root

The step came from Expand, which the simplifier offers as a candidate for the root expression
only, and which does not descend into an exponent, a function's argument or a matrix. So it is written
as a rule instead: a unary minus parses as (-1) * x, so the shape is (-1) * (a - b), and turning it
round drops the multiplication and the negative constant together — five nodes for three, and the
complexity criteria charge four more for a negative real. It lands in InvertNegativeMultipliers,
whose other arm normalises 1 + (-x) into 1 - x: the same family, and already inside the bundle
SimplifyChildren applies everywhere. A rule walks the tree, which is the whole of the fix.

Expand of a matrix is the same story from the other end. It reads its argument as a sum, and a matrix
is not one, so a matrix left through the "if one is too complicated, return the current one" exit and
came back as it arrived. It now expands entry by entry. Factorize and Differentiate both descended
already — they are built out of rewrite rules — so Expand was the odd one out rather than matrices
being held back deliberately.

Where a caller meets it

EquationSystem.Solve returns a Matrix, so a solved system's entries were left in whatever form the
solver built them in:

MathS.Equations("x2 + y", "y - x - 3").Solve("x", "y").Simplify()

was:  [[-(-(5 - sqrt(-11)) / 2 + 3), (5 - sqrt(-11)) / 2], [-(-(5 + sqrt(-11)) / 2 + 3), (5 + sqrt(-11)) / 2]]
is:   [[-1/2 + -1/2 * sqrt(-11), 5/2 + -1/2 * sqrt(-11)], [1/2 * sqrt(-11) + -1/2, 1/2 * sqrt(-11) + 5/2]]

Two corrections to the issue's own analysis

Both would mislead whoever read it next, so they are worth stating rather than quietly fixing.

The sum case's candidate is generated. alternate::-(5 - sqrt(-11)) + y lists sqrt(-11) - 5 + y
second. It lost because the two forms tie: y - (5 - sqrt(-11)) and sqrt(-11) - 5 + y both
rate 22, and a tie is settled by whichever candidate was generated first — the accident the
RationaliseDenominator comment in that same file already warns about. Only the power, sgn and
matrix cases are the "never generated" kind the issue describes.

The complexity table in the issue compared the wrong pair. It read the input (9) against the good
candidate (7) and concluded the good form would win. The chosen output is 7 as well — Simplify did
improve 9 → 7, then picked one of two equally-rated 7s. And Complexity is not what ranks candidates
in any case; SimplifiedRate is, which weights a negative real at 4 and a negative power at 8. I have
added a rate:: probe alongside complexity:: in the workspace harness so this confusion is one
command away from being caught, with the mistake named in its comment.

The rule fixes all four cases including the sum, because b - a is strictly better than either
candidate and so wins the tie outright rather than depending on order.

Measured

The new tests fail 7 of 246 against master and pass 246 of 246 here, checked by restoring
master's two files underneath them.

Suite 6359 passed / 0 failed, F# wrapper 130 passed. casbench 117/119 with 0 wrong, rootcheck 596/596,
simpsweep 10463/10463 — the one that matters most here, since this changes the form of a great many
expressions and simpsweep is what checks a changed form still has the old value. propcheck 1340 checks
with 0 failures, crashcheck 1652 cases with 0 crashes, boundcheck unchanged at 2 disagreements.

Cut from master at fbf1dd77.

-(a - b) became b - a for a whole expression and not for the same expression inside another
node, so what a caller got depended on where the subexpression sat: -(5 - sqrt(-11))
simplified, while the same thing under an exponent, inside sgn, or as a matrix entry did not.

The step came from Expand, which the simplifier offers for the root expression only and which
does not descend into an exponent, a function's argument or a matrix. Written as a rule it runs
wherever the shape occurs, because a rule walks the tree. A unary minus parses as (-1) * x, so
the shape is (-1) * (a - b), and turning it round drops the multiplication and the negative
constant together: five nodes for three, and the metric charges four more for a negative real.
It goes into InvertNegativeMultipliers, whose other arm normalises 1 + (-x) into 1 - x -- the
same family, and already in the bundle SimplifyChildren applies everywhere.

Expand of a matrix is the same story from the other end. It reads its argument as a sum, and a
matrix is not one, so a matrix left by the "too complicated, return what came in" exit and came
back as it arrived. It now expands entry by entry. Factorize and Differentiate both descended
already, being built out of rewrite rules, so Expand was the odd one out rather than matrices
being held back on purpose.

Where a caller meets it: EquationSystem.Solve returns a Matrix, so a solved system's entries
were left in whatever form the solver built them in. The x2 + y system's answer goes from
[[-(-(5 - sqrt(-11)) / 2 + 3), ...]] to [[-1/2 + -1/2 * sqrt(-11), ...]].

Two things in the issue's own analysis were wrong and are worth correcting, since both would
mislead the next reader. The candidate for the sum case *is* generated -- `alternate` lists it
second -- and it was not chosen because the two forms **tie**: both rate 22 and both have
Complexity 7, and a tie is settled by whichever candidate came first. The issue's complexity
table compared the input against the good candidate rather than the chosen output against it.
Only the power, sgn and matrix cases are the "never generated" kind. The rule fixes all four,
the sum case included, because b - a is strictly better than either and so wins the tie outright.

The new tests fail 7 of 246 against master and pass 246 of 246 here. Suite 6359 passed, F#
wrapper 130 passed; casbench 117/119 with 0 wrong; rootcheck 596/596; simpsweep 10463/10463;
propcheck 1340 checks 0 failures; crashcheck 1652 cases 0 crashes; boundcheck unchanged at 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Rafael-SOWNet
Rafael-SOWNet merged commit 8c56b59 into master Aug 12, 2026
25 checks passed
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.

Simplify rewrites the root node only: the same subexpression is left alone inside a matrix, a power or a sum

1 participant