Skip to content

Merge two powers of one base in a product (#781), and guard a zero integration rate (#785) - #786

Merged
Rafael-SOWNet merged 2 commits into
masterfrom
fix/781-merge-powers-of-one-base
Aug 7, 2026
Merged

Merge two powers of one base in a product (#781), and guard a zero integration rate (#785)#786
Rafael-SOWNet merged 2 commits into
masterfrom
fix/781-merge-powers-of-one-base

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

Fixes #781 and #785.

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 -- both elementary, and both answered the moment the same expression is written with the powers already merged.

Two things kept the powers apart

Patterns.PowerRules has the rule, but it pairs two sibling nodes, and a product is a tree rather than a list:

Mulf(Mulf(Powf(sin(x), 4), -6), Powf(sin(x), 2))

The constant sits between the two powers, so they are never siblings and the rule never fires. Separately, Integrate normalises with InnerSimplified, which does not run PowerRules at all -- which is why even x ^ 2 / x, where the two powers are siblings, was missed.

Full Simplify gets these because it reassociates and sorts the factors first. That explains the shape the issue reported: every argument other than a bare x already 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: with a^n read as e^(n Log a) on the principal branch, the two sides are e^(n Log a) * e^(m Log a) and e^((n+m) Log a), equal for every complex n and m. 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:

  • 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 a 1/x^n it is handed into Pow(x, -n). The two normalisations chase each other until the stack runs out -- this aborted the test run before it was found.
  • It rebuilds only when a merge actually happened, since flattening writes every quotient as a negative power and rebuilding unconditionally would rewrite every quotient in the tree.

The second commit is a defect this reaches (#785)

Gathering e^x * e^(-x) produces e^(x + -x), whose linear rate is zero while x + -x is 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:

"e ^ (x + -x)".ToEntity().Integrate("x")   // e ^ (x + -x) / (0 * ln(e))  -- NaN, it is x

This is on master already -- writing the exponent out reaches it without any of the above -- but this PR makes it reachable from ordinary input, and without the guard sinh(x)^2 - cosh(x)^2 answers NaN where master answers correctly. So it is fixed here rather than left to be found later.

Each of those integrands depends on x only through the argument whose rate is read, so a zero rate means the integrand is a constant and integrates to itself times x. One guard ahead of the table answers the family. The rate has to be decidably zero -- a symbolic one is not, and answering sin(a * x) * x would be wrong for every non-zero a.

Measured

integrand before after
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
e ^ (x + -x) NaN x
sin(x + -x) NaN 0
x ^ 2 * x answered unchanged
sin(2 * x) ^ 4 * (-6) * sin(2 * x) ^ 2 answered unchanged
e ^ (2 * x), sin(3 * x + 1) answered unchanged

Harnesses, against master (e05d7179):

  • unit tests 5418 pass, 0 fail; F# 130/130
  • casbench 113/117, 0 wrong / 0 error / 0 timeout -- unchanged
  • rootcheck 596/596 clean; simpsweep 10463/10463 agree
  • propcheck 1337 -> 1340 checks, 0 failures either side: three more integrals answer, all of them correct

Performance, on the Tests.Calculus subset (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

🤖 Generated with Claude Code

Rafael-SOWNet and others added 2 commits August 7, 2026 17:23
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
Rafael-SOWNet merged commit a3d3b71 into master Aug 7, 2026
24 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the fix/781-merge-powers-of-one-base branch August 7, 2026 21:30
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>
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.

Two powers of one base in a product are never merged, so an integrand that is a power in disguise is declined

1 participant