You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
CollectLikeTerms fails on the very example its documentation cites #855
Split out of #852, where it was found and deliberately not folded in. Pre-existing, not a 2.0 regression — reproduced on master before that change and after it.
The case
CollectLikeTerms carries this doc comment:
Adds up the terms of an expanded sum that differ only by a numeric factor, so that expanding actually finishes: (x+1)^2 * (x+1)^2 multiplied out gives sixteen terms, of which only five are distinct.
That exact expression is the one it does not finish:
2x^2 + 4x^2 is left standing. The answer is correct — the coefficients sum to 16, so no term is lost or duplicated — but the collection that the method exists to perform did not happen, and the result is one term longer than it should be.
Identical at Expand(1), Expand(2) and Expand(3), so it is not a question of passes.
Why it is worth chasing
The three neighbouring forms all collect correctly, so whatever is different about the same base squared, multiplied by itself is narrow. Two terms that both mean x^2 are landing under different monomial keys, and the key is built from the exponent dictionary, so the exponents are being reached by two routes that do not agree — most likely one term arriving as x^2 and another as x * x and the two not reducing to the same key.
Anything that reduces to a monomial by two different routes is a candidate for the same fault, so the fix is probably worth more than this one expression.
Deliberately not fixed for 2.0
Recommending this does not block the release. It produces a correct answer that is one term longer than it should be, which is a quality defect rather than a wrong answer, and CollectLikeTerms sits in the middle of expansion and simplification — not somewhere to make a hurried change days before a major version. It wants a proper look and a test over the routes into a monomial key, not a patch.
Root cause found, a fix written, and then withdrawn — because it trades this defect for a worse one. Recording all of it so the next attempt starts from here rather than from scratch.
The cause
Instrumenting CollectLikeTerms to print each term's monomial key gives it in one line:
[collect] term=1 key='' coeff=1
[collect] term=2 * x key='x^1' coeff=2
[collect] term=x ^ 2 key='x^2' coeff=1
[collect] term=(2 * x) ^ 2 key='4 * x ^ 2^1' coeff=1 <-- here
[collect] term=2 * x * x ^ 2 key='x^3' coeff=2
...
(2 * x)^2 reduces to 4 * x^2, which is a product, not a power. So it falls to the (reduced, exponent 1) branch, and the numeric 4 is baked into the monomial key — 4 * x ^ 2 — instead of being extracted into the coefficient. Keyed that way it cannot meet the genuine x^2 terms, and the two sit side by side in the output.
That is the whole of it: a numeric factor that fails to reach the coefficient.
The fix that works, and what it costs
Splitting a reduced factor again when it is a product, and distributing an integer power over a product, fixes every polynomial case:
before
after
(x+1)^2 * (x+1)^2
1 + 4x + 2x² + 4x² + 4x³ + x⁴
1 + 4x + 6x² + 4x³ + x⁴
(x+y)^2 * (x+y)^2
… + 2x²y² + (2xy)² + …
… + 6x²y² + …
(2x+1)^2 * (2x+1)^2
uncollected
1 + 8x + 24x² + 32x³ + 16x⁴
(3x)^2 + x^2
uncollected
10x²
And it breaks DoubleAngleOverSineTest.TheReportersSecondExpressionIsAlsoZero.(sin(t)·cos(t)·csc(t))² - cos(t)² stops simplifying to 0. The instrumentation shows why:
Pulling the 1/2 out to the coefficient — precisely what the fix is for — leaves sin(2t)² · csc(t)² as the monomial, and that no longer cancels against the -cos(t)² beside it. Left whole, (1/2·sin(2t))²·csc(t)² reassembles and InnerSimplified cancels it.
I tried restricting the split to products carrying a number, and separately to integer coefficients only. The first does not help — 1/2is a number. The second works but is a heuristic with no principle behind it, and I am not willing to put one of those in the middle of simplification.
So the two goals are in tension
Collecting wants numeric factors out of the monomial, so terms meet.
Trigonometric cancellation wants the product left whole, so InnerSimplified can collapse it.
A fix has to satisfy both, which means it is a design question rather than a patch. The obvious shape is to try the collected form and keep it only when it is actually shorter — the same "offer a candidate rather than take it" pattern Simplificator already uses for the boolean minimiser and for RationaliseDenominator — but that is a change to how CollectLikeTerms returns, not a line inside it.
Still recommending this does not block 2.0
Unchanged from the issue above, and now better evidenced: the current behaviour gives a correct answer that is one term too long, and the alternative I have in hand gives a wrong answer on a case that has a regression test. The first is a quality defect; the second is the kind of change that produced #851.
Activity
Rafael-SOWNet commentedon Aug 9, 2026
Root cause found, a fix written, and then withdrawn — because it trades this defect for a worse one. Recording all of it so the next attempt starts from here rather than from scratch.
The cause
Instrumenting
CollectLikeTermsto print each term's monomial key gives it in one line:(2 * x)^2reduces to4 * x^2, which is a product, not a power. So it falls to the(reduced, exponent 1)branch, and the numeric4is baked into the monomial key —4 * x ^ 2— instead of being extracted into the coefficient. Keyed that way it cannot meet the genuinex^2terms, and the two sit side by side in the output.That is the whole of it: a numeric factor that fails to reach the coefficient.
The fix that works, and what it costs
Splitting a reduced factor again when it is a product, and distributing an integer power over a product, fixes every polynomial case:
(x+1)^2 * (x+1)^21 + 4x + 2x² + 4x² + 4x³ + x⁴1 + 4x + 6x² + 4x³ + x⁴(x+y)^2 * (x+y)^2… + 2x²y² + (2xy)² + …… + 6x²y² + …(2x+1)^2 * (2x+1)^21 + 8x + 24x² + 32x³ + 16x⁴(3x)^2 + x^210x²And it breaks
DoubleAngleOverSineTest.TheReportersSecondExpressionIsAlsoZero.(sin(t)·cos(t)·csc(t))² - cos(t)²stops simplifying to0. The instrumentation shows why:Pulling the
1/2out to the coefficient — precisely what the fix is for — leavessin(2t)² · csc(t)²as the monomial, and that no longer cancels against the-cos(t)²beside it. Left whole,(1/2·sin(2t))²·csc(t)²reassembles andInnerSimplifiedcancels it.I tried restricting the split to products carrying a number, and separately to integer coefficients only. The first does not help —
1/2is a number. The second works but is a heuristic with no principle behind it, and I am not willing to put one of those in the middle of simplification.So the two goals are in tension
InnerSimplifiedcan collapse it.A fix has to satisfy both, which means it is a design question rather than a patch. The obvious shape is to try the collected form and keep it only when it is actually shorter — the same "offer a candidate rather than take it" pattern
Simplificatoralready uses for the boolean minimiser and forRationaliseDenominator— but that is a change to howCollectLikeTermsreturns, not a line inside it.Still recommending this does not block 2.0
Unchanged from the issue above, and now better evidenced: the current behaviour gives a correct answer that is one term too long, and the alternative I have in hand gives a wrong answer on a case that has a regression test. The first is a quality defect; the second is the kind of change that produced #851.
Let a numeric factor reach the coefficient when collecting like terms (…