Merge two powers of one base in a product (#781), and guard a zero integration rate (#785) - #786
Merged
Merged
Conversation
Every rule in the standard-integral table reads a linear rate out of its
integrand's argument and divides by it. An argument that mentions x without
depending on it has a rate of zero, and the division went into the answer
unguarded: `e ^ (x + -x)` integrated to `e ^ (x + -x) / (0 * ln(e))` and
`sin(x + -x)` to `-cos(x + -x) / 0`, both of which evaluate to NaN. Both
integrands are constants.
Each of those integrands depends on x only through the argument whose rate is
being read, so a zero rate means the whole integrand is constant and integrates
to itself times x. One guard ahead of the table answers the family, rather than
a condition repeated on each of the fourteen rules.
The rate must be *decidably* zero. A symbolic one is not: answering
`sin(a * x) * x` would be wrong for every non-zero a, so the guard reads the
evaluated rate and withdraws unless it is a literal zero.
e ^ (x + -x) NaN -> e ^ (x + -x) * x
sin(x + -x) NaN -> sin(x + -x) * x
e ^ (2 * x) unchanged
sin(3 * x + 1) unchanged
propcheck 1337 -> 1340 checks, 0 failures throughout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An integrand that is a power in disguise was declined: `x ^ 2 / x` came back as
an unresolved integral, and `sin(x) ^ 4 * (-6) * sin(x) ^ 2` was not answered at
all, though both are elementary and both answer the moment the same expression
is written with the powers already merged.
Two things kept them apart. `PowerRules` pairs two sibling nodes, and a product
is a tree rather than a list, so the constant sitting between the two powers
makes them non-siblings and the rule never fires. And `Integrate` normalises its
input with `InnerSimplified`, which does not run `PowerRules` at all -- so even
`x ^ 2 / x`, where the two powers *are* siblings, was missed. Full `Simplify`
gets these only because it reassociates and sorts the factors first, which is
why every argument other than a bare x already answered: those go through a
substitution that rebuilds the tree on the way past.
Gathering walks the flattened factors and keys them by base, so it reaches
through any number of intervening factors rather than one. `a^n * a^m = a^(n+m)`
needs no condition -- with `a^n` read as `e^(n Log a)` the two sides are equal
for every complex n and m, which is what makes it unlike `(a^b)^c = a^(b*c)`.
Two things it must not do, both measured rather than reasoned about. Only the
exponents it builds are folded, never the product around them: `InnerSimplified`
on the whole expression rewrites `x^(-2)` back into `1/x^2` and
`SolveAsPolynomialTerm` rewrites that into `Pow(x, -2)` again, and the two chase
each other until the stack runs out. And it rebuilds only when a merge actually
happened, since flattening writes every quotient as a negative power.
x ^ 2 / x declined -> x ^ 2 / 2
x ^ 2 * (1 / x) declined -> x ^ 2 / 2
x ^ 3 / x ^ 2 declined -> x ^ 2 / 2
sin(x) ^ 4 * (-6) * sin(x) ^ 2 declined -> answered
sin(2 * x) ^ 4 * ... unchanged
Needs #785, which this reaches: gathering `e^x * e^(-x)` into `e^(x + -x)` puts
a zero-rate argument in front of the exponential rule, and without that guard
`sinh(x)^2 - cosh(x)^2` answers NaN where master answers correctly.
casbench 113/117 unchanged, 0 wrong; rootcheck 596/596; simpsweep 10463/10463;
propcheck 1340 checks 0 failures. Calculus tests 860 in 56s -> 880 in 55s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 7, 2026
PR #758 (#779) pinned four integrands as a *separate* known gap, declined rather than hung, so that they would not be read as the defect that file is about. PR #786 (#781) fixed that gap, and the two were cut independently from master, so neither could carry the other's half of this. The assertion is inverted rather than deleted, because these four are the evidence that the two changes compose: distributing `sin(x)^4 * (5 - 6 sin(x)^2)` over its sum is what *produces* `sin(x)^4 * (-6) * sin(x)^2`, and merging the two powers is what makes that answerable. Neither PR could show that on its own, and a deleted test would have left it unshown. 5442 pass, 0 fail on merged master. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #781 and #785.
An integrand that is a power in disguise was declined.
x ^ 2 / xcame back as an unresolved integral, andsin(x) ^ 4 * (-6) * sin(x) ^ 2was not answered at all -- both elementary, and both answered the moment the same expression is written with the powers already merged.Two things kept the powers apart
Patterns.PowerRuleshas the rule, but it pairs two sibling nodes, and a product is a tree rather than a list:The constant sits between the two powers, so they are never siblings and the rule never fires. Separately,
Integratenormalises withInnerSimplified, which does not runPowerRulesat all -- which is why evenx ^ 2 / x, where the two powers are siblings, was missed.Full
Simplifygets these because it reassociates and sorts the factors first. That explains the shape the issue reported: every argument other than a barexalready answered, because those go through a substitution that happens to rebuild the tree on its way past. The merge was only ever reached by accident.What this does
Gathering walks the flattened factors and keys them by base, so it reaches through any number of intervening factors rather than one.
a^n * a^m = a^(n+m)needs no condition: witha^nread ase^(n Log a)on the principal branch, the two sides aree^(n Log a) * e^(m Log a)ande^((n+m) Log a), equal for every complexnandm. That is what makes it unlike(a^b)^c = a^(b*c), which moves the branch and is guarded (#752).Two things it must not do, both found by measurement rather than by reading:
InnerSimplifiedon the whole expression rewritesx^(-2)back into1/x^2, andSolveAsPolynomialTermrewrites a1/x^nit is handed intoPow(x, -n). The two normalisations chase each other until the stack runs out -- this aborted the test run before it was found.The second commit is a defect this reaches (#785)
Gathering
e^x * e^(-x)producese^(x + -x), whose linear rate is zero whilex + -xis still written out. Every rule in the standard-integral table divides by that rate unguarded, so the answer came back with a literal division by zero in it:This is on
masteralready -- writing the exponent out reaches it without any of the above -- but this PR makes it reachable from ordinary input, and without the guardsinh(x)^2 - cosh(x)^2answersNaNwheremasteranswers correctly. So it is fixed here rather than left to be found later.Each of those integrands depends on
xonly through the argument whose rate is read, so a zero rate means the integrand is a constant and integrates to itself timesx. One guard ahead of the table answers the family. The rate has to be decidably zero -- a symbolic one is not, and answeringsin(a * x) * xwould be wrong for every non-zeroa.Measured
x ^ 2 / xx ^ 2 / 2x ^ 2 * (1 / x)x ^ 2 / 2x ^ 3 / x ^ 2x ^ 2 / 2sin(x) ^ 4 * (-6) * sin(x) ^ 2e ^ (x + -x)NaNxsin(x + -x)NaN0x ^ 2 * xsin(2 * x) ^ 4 * (-6) * sin(2 * x) ^ 2e ^ (2 * x),sin(3 * x + 1)Harnesses, against
master(e05d7179):casbench113/117, 0 wrong / 0 error / 0 timeout -- unchangedrootcheck596/596 clean;simpsweep10463/10463 agreepropcheck1337 -> 1340 checks, 0 failures either side: three more integrals answer, all of them correctPerformance, on the
Tests.Calculussubset (two runs each): master 860 tests in 56 s and 57 s; this branch 880 tests in 55 s and 55 s. The gathering is a tree walk on every recursive integrate call and does not show up.Notes for review
LinearityBeforeByPartsTest.PowersOfOneBaseAreNotMergedin Distribute over a sum before searching by parts (#779) #782 pins the old behaviour deliberately and will need removing when both land. This branch is cut frommasterand does not contain it.abscase of A zero linear rate is divided by, so Integrate answers NaN for an argument that mentions x without depending on it #785 is checked on the antiderivative rather than by differentiating it back: the library differentiatesabs(u)assgn(u) * u'and answersNaNatu = 0, so the round trip says nothing either way. That is a separate quirk and is not touched here.🤖 Generated with Claude Code