Skip to content

Factor polynomials with whole roots (#177) - #669

Merged
Happypig375 merged 2 commits into
ASC-Community:masterfrom
Rafael-SOWNet:fix/polynomial-factoring
Aug 3, 2026
Merged

Happypig375 merged 2 commits into
ASC-Community:masterfrom
Rafael-SOWNet:fix/polynomial-factoring

Conversation

@Rafael-SOWNet

@Rafael-SOWNet Rafael-SOWNet commented Aug 3, 2026 •

Copy link
Copy Markdown
Member

Closes #177: x^2 + 2x + 1
stayed as it was written.

x^2 + 2x + 1          →  (1 + x)^2
x^3 + 3x^2 + 3x + 1   →  (1 + x)^3
x^3 - 6x^2 + 11x - 6  →  (x - 1) * (x - 2) * (x - 3)
2x^2 + 4x + 2         →  2 * (1 + x)^2
x^2 + 2x              →  x * (2 + x)

A polynomial in one variable with rational coefficients has its rational roots found by
the rational root theorem — divisors of the constant term over divisors of the leading
one — each checked by exact evaluation and divided out by exact synthetic division. No
floating point is involved, so a root is either exact or not a root.

The factored form is handed to the simplifier as one more candidate, not as a
decision. It sits alongside the other forms already collected and the complexity metric
picks, which is why (1 + x)^2 wins while x^2 - 1 stays as it is.

Deliberately narrow, on two counts

Both are load-bearing — an earlier draft that only required a root to be found
regressed real cases.

Rational roots only. Factoring through every root would answer (x - i)(x + i) for
x^2 + 1 and (x - sqrt(2))(x + sqrt(2)) for x^2 - 2, which is not what factoring
those means. Tests assert those are left alone.

And only when it splits completely into whole roots. A partly factored answer is not
obviously better than the sum it came from, and fractional roots turn up mostly in the
output of calculus, where the expanded form is the conventional one — the antiderivative
of x^2 + x reads better as x^3/3 + x^2/2 than as x^2 * (x + 3/2) / 3. That was a
real regression in the earlier draft, and there is a test for it now.

Multivariate expressions are untouched and still go to the term-collecting rules.

Two existing tests change, and one is removed

  • BigSimple1 asserted the expanded form of (1 + x)^4. It now gets (1 + x)^4,
    which is what it was asking for.

  • FactorialXM1OverFactorialXM3 gets the same two factors in the other order.

  • TestPowerSubstitution's second case is removed, because it was pinning a wrong
    answer:

    ∫ 3x²(x³+2)² dx  →  C - 5/4·x⁶ + (x³+2)(x⁶/2 + 2x³)
    

    That is not an antiderivative of the integrand. At x = 2 the integrand is 1200
    and the derivative of that answer is 1536. The true antiderivative is
    (x³+2)³/3, which differentiates back exactly. I checked this on a stock master
    build — it is not caused by this PR; this PR only perturbs a wrong answer into a
    differently wrong one, which is what surfaced it.

    Rather than update the expectation to another wrong string, the case is restated as
    what it should assert — that differentiating the answer returns the integrand — and
    left Skipped, matching how the other unfinished integration cases in that file are
    handled. It is checked at points rather than symbolically, because the difference is
    zero but Simplify does not reduce it to zero.

    It passes once Fix two Simplify hangs, an integration stack overflow, and incomplete factoring (#403, #531, #178, #205) #665 is also applied. I bisected this: neither Fix two Simplify hangs, an integration stack overflow, and incomplete factoring (#403, #531, #178, #205) #665 nor this PR
    fixes it alone, but together they do — with factoring available, the Simplify(1)
    inside the integrator produces a form that lets u-substitution succeed, so the
    integrand takes the correct route instead of the faulty by-parts one. The by-parts
    defect is presumably still there, just no longer reached for this integrand. Once
    both land, the skip can come off.

Testing

UnitTests 3675 passed / 0 failed. On the 117-problem corpus this branch is 76 solved
against master's 75, with one fewer wrong answer.

18 new tests: the factored cases, value preservation for each (including the ones that
deliberately do not factor), the irrational and complex cases being left alone, the
partial-split case, the antiderivative case, and the multivariate case.

Independent of #663 through #668; touches no file any of them touch.

🤖 Generated with Claude Code

@codecov-commenter

codecov-commenter commented Aug 3, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 92.06349% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.95%. Comparing base (90c00a8) to head (6453674).
⚠️ Report is 48 commits behind head on master.

Files with missing lines Patch % Lines
...th/Functions/Simplification/PolynomialFactoring.cs 91.86% 4 Missing and 6 partials ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #669      +/-   ##
==========================================
- Coverage   80.99%   79.95%   -1.05%     
==========================================
  Files         155      156       +1     
  Lines       13687    12602    -1085     
  Branches     1957     2055      +98     
==========================================
- Hits        11086    10076    -1010     
+ Misses       1990     1932      -58     
+ Partials      611      594      -17     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines +93 to +94
[Fact(Skip = "Integration returns a wrong antiderivative here on its own; " +
"it is right once the integration-recursion fix is also applied")]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs an update

Rafael-SOWNet and others added 2 commits August 3, 2026 18:52
`x^2 + 2x + 1` stayed as it was written. Factoring it wants the rational root
theorem, which the library did not have.

A polynomial in one variable with rational coefficients now has its rational roots
found -- divisors of the constant term over divisors of the leading one, checked by
exact evaluation and divided out by exact synthetic division -- and the factored form
is handed to the simplifier as one more candidate. The simplifier keeps it alongside
the other forms and picks by its complexity metric, which is why `(1 + x)^2` wins
while `x^2 - 1` stays as it is.

Deliberately narrow, on two counts, both of which are load-bearing:

  - Rational roots only. Factoring through every root would answer (x - i)(x + i)
    for x^2 + 1 and (x - sqrt(2))(x + sqrt(2)) for x^2 - 2, which is not what anyone
    means by factoring those.

  - And only when the polynomial splits completely into whole roots. A partly
    factored answer is not obviously better than the sum it came from, and
    fractional roots turn up mostly in the output of calculus, where the expanded
    form is the conventional one: the antiderivative of x^2 + x reads better as
    x^3/3 + x^2/2 than as x^2 * (x + 3/2) / 3. Both of those were regressions in an
    earlier draft of this that only required *a* root to be found.

On the corpus: 75 -> 76 solved and one fewer wrong answer.

Two existing tests change. BigSimple1 asserted the expanded form of (1 + x)^4 and
now gets (1 + x)^4, which is what it was asking for. FactorialXM1OverFactorialXM3
gets the same two factors in the other order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The project multi-targets netstandard2.0 and net7.0, and the unit tests only build
the latter, so two net7-only conveniences got through: KeyValuePair deconstruction
and the range operator, which needs RuntimeHelpers.GetSubArray. The F# wrapper build
is what caught them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Rafael-SOWNet

Copy link
Copy Markdown
Member Author

Updated, and rebased onto current master.

The skipped test is gone rather than updated. It only ever existed because this branch, on its own, perturbed 3 * x2 * (x3 + 2) ^ 2 from one wrong antiderivative into a different wrong one, and I did not want to pin either — so I moved the case out of the theory into a skipped test asserting what it should give.

#670 fixed the actual cause (Variable.CreateUnique handing back a name already in use), and its version of TestPowerSubstitution now covers that case in the theory with the closed form C + (x ^ 3 + 2) ^ 3 / 3. So the stand-in has nothing left to stand in for, and master's theory is kept as-is.

Suite is green on the rebase: 3841 passed, 0 failed.

@Rafael-SOWNet
Rafael-SOWNet force-pushed the fix/polynomial-factoring branch from da2eb2e to 6453674 Compare August 3, 2026 18:56
@Happypig375
Happypig375 merged commit 079f341 into ASC-Community:master Aug 3, 2026
21 of 24 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the fix/polynomial-factoring branch August 4, 2026 20:49
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.

Cannot collapse x^2+2x+1 into (x+1)^2

3 participants