Skip to content

A negative power of the variable is not a polynomial to integration by parts - #1647

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
a-negative-power-is-not-a-polynomial
Oct 1, 2026
Merged

Rafael-SOWNet merged 1 commit into
masterfrom
a-negative-power-is-not-a-polynomial

Conversation

@Rafael-SOWNet

@Rafael-SOWNet Rafael-SOWNet commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

MathS.TryPolynomial reads x^(-1) as a monomial of degree -1. Two rules differentiated what it read until it reached 0: tabular integration by parts, which allows 2000 rounds, and the loop for a polynomial times an exponential and a sine or a cosine, which allows 64. A negative power never reaches 0, and each derivative was a larger tree than the last. So e^(2x) x^(-1) ran the process out of memory within seconds, where e^(2x)/x is Ei(2x) in milliseconds. The integrator writes a quotient this way itself once a constant factor is out.

Both rules now take only a polynomial with no negative degree, and so does the check that lets parts go on past a special function, which relies on the degree falling. Past them, by parts differentiates the power, and the remainder is the quotient the exponential integral rules read.

The other readers of TreeAnalyzer.TryGetPolynomial in the library already refuse a negative degree: the sums and series over an index, the image of a polynomial and the Gaussian moments. Those integrator readings that still accept one either feed a gate that now refuses it (the regrouping before TrySplit) or take a single step of parts (a logarithm beside a polynomial).

Measured

Input Was (2.5.0) Now
"e^(2*x)*x^(-1)".Integrate("x") out of memory: OutOfMemoryException under a 2 GB heap 1 / x * e ^ (2 * x) / 2 - -1/2 * (-1 / x * e ^ (2 * x) + 2 * Ei(2 * x)) + C, which is Ei(2 * x) + C
"e^x*x^(-2)".Integrate("x") integral(e ^ x * x ^ (-2), x) e ^ x * -1 / x - -Ei(x) + C
  • Probes on master at 492afc23, one integral per process, killed at 2 GB or 60 s: 11 of 26 integrals ran away (8 out of memory, 3 past 60 s). 23 of the 26 hold a negative power, and the rest are controls. Here all 26 finish.
    • Answered, each differentiated back: e^(2x) x^(-1), e^(a x) x^(-1), e^x x^(-2), x^(-3) e^x, x^(-2) e^(3x), (x + x^(-1)) e^x, 2^x x^(-1), and the same in t.
    • Declined in under half a second: e^x sin(x) x^(-1) and e^x cos(x) x^(-2).
  • Rubi, master at 492afc23 and this change on it, run side by side, 0 wrong everywhere:
    • Family 3 at twenty a file: 171 → 173 of 180.
    • Families 1 and 5–7 at five a file: 438 → 439 of 499.
    • Family 2: 544 of 650 on both, with four timeouts fewer.
    • Family 4 at ten a file: 611 of 696 on both.
    • Family 8: 395 of 420 on both.
    • The independent suites: 1756 of 1814 on both.
  • Each problem that moved, run alone in a process of its own:
    • ln(c (a + b x^3)^p)/x^7 is answered in 0.4 s, and x^(3/2) (A + B x)/(a + b x + c x^2) in 14 s. Master runs out the budget on both, and on the second it is still running after three minutes.
    • F^(k sqrt(1 - a x)/sqrt(1 + a x))/(1 - a^2 x^2), for four k, is declined in 5 to 8 s. On master the worker passes 4 GB and is killed. During these runs a guard killed six of master's workers past 4 GB and none of this change's.
    • The seventh, (a + b x)^3/((c + d x)^5 ln(e ((a + b x)/(c + d x))^n)), is answered on both in 22 s alone. Its side-by-side difference was load.
  • The unit tests pass: 14,275, none failed (net10.0), with the GC heap held to 4 GB. The native AOT publish of the C++ wrapper builds with no trim or AOT warnings.
  • The performance gate passes on 0680a2f0, which is this change but for comments and the changelog. Allocation matches the baseline on all 19 gated benchmarks.

ANegativePowerIsNotAPolynomialTest integrates nine such products under a guard and differentiates each answer back on both sides of the pole. Against master, two run out of memory under a 2 GB heap, a third outlasts the guard, and the run aborts.

Closes #1646.

🤖 Generated with Claude Code

…y parts

MathS.TryPolynomial reads x^(-1) as a monomial of degree -1, and two rules differentiated what it
read until it reached 0: tabular integration by parts, which allows two thousand rounds, and the
loop for a polynomial times an exponential and a sine or a cosine, which allows sixty-four. A
negative power never reaches 0, and each derivative was a larger tree than the last, so
e^(2x) x^(-1) ran the process out of memory within seconds, where e^(2x)/x is Ei(2x) in
milliseconds. The power is no unusual spelling: the integrator writes a quotient that way itself
once a constant factor is out.

Both rules now take a polynomial none of whose degrees is negative, and so does the check that lets
parts go on past a special function, which counts on the degree falling. Past them, by parts takes
the power as the factor to differentiate, and the remainder is the quotient the exponential
integral rules read: e^(2x) x^(-1), e^(a x) x^(-1), e^x x^(-2), x^(-3) e^x, (x + x^(-1)) e^x and
2^x x^(-1) are answered, each out of memory or past a minute on master, and e^x sin(x) x^(-1) and
e^x cos(x) x^(-2) are declined in under half a second.

Measured on the Rubi corpus, master at 492afc2 and this change on it, run side by side: family 3 at
twenty a file 171 -> 173 of 180, families 1 and 5 to 7 at five a file 438 -> 439 of 499, family 2
544 of 650 on both with four timeouts fewer, family 4 at ten a file 611 of 696, family 8 395 of 420
and the independent suites 1756 of 1814 on both. 0 wrong everywhere. Each problem that moved, run
alone in a process of its own: ln(c (a + b x^3)^p)/x^7 is answered in 0.4 s and
x^(3/2) (A + B x)/(a + b x + c x^2) in 14 s, where master runs out the budget, the second past three
minutes; F^(k sqrt(1 - a x)/sqrt(1 + a x))/(1 - a^2 x^2), for four k, is declined in 5 to 8 s, where
master's worker passes 4 GB and is killed. The seventh,
(a + b x)^3/((c + d x)^5 ln(e ((a + b x)/(c + d x))^n)), is answered on both in 22 s alone; side by
side it was load. The unit tests pass, 14,275, with the GC heap held to 4 GB, and the performance
gate passes on 0680a2f0, which is this change but for comments and the changelog: allocation is what
the baseline says on all 19 gated benchmarks.

ANegativePowerIsNotAPolynomialTest integrates nine such products under a guard and differentiates
each answer back on both sides of the pole. Against master, two run out of memory under a 2 GB
heap, a third outlasts the guard, and the run aborts.

Closes #1646.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Integrating e^(2x) x^(-1) runs out of memory: integration by parts reads x^(-1) as a polynomial

1 participant