Skip to content

CollectLikeTerms fails on the very example its documentation cites #855

Closed

Description

@Rafael-SOWNet

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:

(x+1)^2 * (x+1)^2         ->  1 + 4x + 2x^2 + 4x^2 + 4x^3 + x^4     six terms
(x+1)^4                   ->  1 + 4x + 6x^2 + 4x^3 + x^4            five, correct
(x+1)*(x+1)*(x+1)*(x+1)   ->  x^4 + 4x^3 + 6x^2 + 4x + 1            five, correct
(x+1)^2*(x+2)^2           ->  4 + 12x + 13x^2 + 6x^3 + x^4          five, correct

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.

Activity

  1. Rafael-SOWNet commented on Aug 9, 2026

    @Rafael-SOWNet
    MemberAuthor

    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:

    term=(1/2 * sin(2 * t)) ^ 2 * csc(t) ^ 2   coeff=1/4   keys=[csc(t), sin(2 * t)]
    

    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/2 is 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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions