Skip to content

A repeated quadratic denominator is integrated (#180) - #1155

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
repeated-quadratic-integrals
Sep 3, 2026
Merged

Rafael-SOWNet merged 1 commit into
masterfrom
repeated-quadratic-integrals

Conversation

@Rafael-SOWNet

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

Copy link
Copy Markdown
Member

1/(x^2 + 1)^2 had no antiderivative. 1/(x^2 - 1)^2 and 1/(x^2 + 2x + 1)^2 both did — a
denominator with real roots comes apart into linear factors and never reached the gap — so what was
missing was exactly the irreducible case.

"1/(x^2 + 1)^2".Integrate("x")     was  integral(1 / (1 + x ^ 2) ^ 2, x)
                                    is  arctan(x) / 2 + C + 1/2 * x / (x ^ 2 + 1)
"x^2/(x^2 + 2)^2".Integrate("x")   was  integral(x ^ 2 / (x ^ 2 + 2) ^ 2, x)
                                    is  the antiderivative
"(x + 3)/(x^2 + 2x + 5)^2"         was  unevaluated
                                    is  arctan((x + 1) / 2) / 8 + C - 1/4 / (5 + x ^ 2 + 2 * x)
                                          + x / (4 * (5 + x ^ 2 + 2 * x))

The reduction is an identity, not a case analysis

Write Q for the quadratic, u for its derivative 2ax + b, D for 4ac - b^2. Then u^2 is
4aQ - D, so differentiating u/Q^(m-1) gives 2a(3 - 2m)/Q^(m-1) + (m-1)D/Q^m, and reading that
as an equation for the integral of 1/Q^m leaves

J_m = (u / Q^(m-1) + 2a(2m - 3) J_(m-1)) / ((m - 1) D)

unrolled down to J_1, which the table already answered. One line serves both signs of the
discriminant
, since it is an identity in a, b and c rather than a fact about signs. D = 0
is the sole exclusion and the division by D is exactly why.

The two shapes the reduction cannot speak for get their own arms — a zero leading coefficient leaves
a linear power, a zero discriminant leaves a power of u. Both are already answered for a numeric
quadratic by machinery that exists; the arms are there so a symbolic one, whose discriminant sign
cannot be decided and whose arms therefore cannot be dropped, is answered as a piecewise instead of
carrying a division by zero into the result.

A numerator of any degree

N = qQ + r takes one power off the denominator and two degrees off the numerator, and ends.

Improper fractions are declined rather than divided out here. The recursion bottoms out at a
single power with a numerator still of degree two or more, which is precisely the case
SolveByPartialFractions already opens with PolynomialLongDivision. Declining hands it there
rather than answering the same thing twice, in two shapes, from two places.

Two things measurement caught that reading would not have

PolynomialLongDivision returns the remainder already divided by the divisor. x^2 over
x^2 + 2 answers (1, -2/(x^2 + 2)), not (1, -2). I had assumed the latter, fed it back as a
numerator, and it is not a polynomial — so the next division declined and every polynomial-numerator
case silently fell through. The remainder is now computed as N - qQ, which does not depend on the
shape the division chooses to report. Found by instrumenting, after reasoning about the guard chain
gave the wrong answer twice.

A term dropped rather than multiplied by zero. A numerator already along the denominator's
derivative leaves no second integral. Multiplying it out is not harmless: 0 * (2x/(x^2 + 1)) is
0 provided not 1 + x^2 = 0, correctly, since the factor has no value there — and that condition
would then be attached to the answer for x/(x^2 + 1)^2, which had none before and is owed none.

What changes that already worked

A denominator with real roots now takes this route rather than partial fractions. Same value,
one logarithm rather than two, which is why the node-count metric that ranks candidates selects it.
No test pinned the old spelling; I found it by probing, and it is in BREAKING-CHANGES.md with both
strings measured on a build.

I considered guarding the rule off a decidably-negative discriminant to leave those to partial
fractions, and rejected it: it would make the concrete and the symbolic path disagree about which
machinery answers the same shape, which is the kind of split that hides defects.

The cost, stated rather than buried

Both arms on one machine, master and branch, measured the same way.

master now
1/(x^2 + 1)^2 2879 ms, unevaluated 111 ms, answered
x^2/(x^2 + 2)^2 92 990 ms, unevaluated 565 ms, answered
1/(x^4 + 1)^2 3811 ms 3870 ms — base is not quadratic, nothing changes
ln(x)/(x^2 + 1)^2 15 034 ms, unevaluated 51 072 ms, unevaluated
sin(x)/(x^2 + 1)^2 26 492 ms, unevaluated 114 851 ms, unevaluated
e^x/(x^2 + 1)^2 7944 ms, unevaluated 85 683 ms, unevaluated

Where the integrand is rational this is 26 to 165 times faster, and the reason is structural rather
than lucky: TryStandardIntegrals runs before every search, so a shape answered outright never
enters the candidate exploration where this family's cost has always been.

Where it is a transcendental function over a repeated quadratic — none of which has an elementary
antiderivative — the search takes 3.4 to 10.8 times longer to conclude nothing.
The sub-integral it
used to fail on immediately now succeeds, and integration by parts carries on from a larger
expression. Those integrands never match this rule; the growth is in by-parts, which every integral
goes through, so bounding it is its own change with its own measurement and I have filed it as
#1156 rather than bundling it here. A reviewer may reasonably want that bound first — the
absolute numbers on that group are bad on master too, and this makes them worse.

Verification

Full suite 9515 passed, 0 failed, 14 skipped. 26 new tests.

Every answer is differentiated back and compared with the integrand. The helper's guard on
integral( is load-bearing rather than decorative: differentiating an unevaluated integral(f, x)
gives back f, so without it every declined case would "verify". One case is checked exactly, and
the residual comes back as 0 under a condition rather than bare 0 — right, rather than a
shortfall, since both the integrand and its antiderivative are undefined where 1 + x^2 = 0, and an
unconditional zero would claim agreement where neither has a value.

Part of #180.

`1/(x^2 + 1)^2` had no antiderivative, while `1/(x^2 - 1)^2` and
`1/(x^2 + 2x + 1)^2` both did: a denominator with real roots comes apart into
linear factors and never reached the gap. What was missing was the irreducible
case.

Writing Q for the quadratic, u for its derivative 2ax + b and D for 4ac - b^2,
the identity u^2 = 4aQ - D turns the derivative of u/Q^(m-1) into an equation
for the integral of 1/Q^m:

    J_m = (u / Q^(m-1) + 2a(2m - 3) J_(m-1)) / ((m - 1) D)

unrolled down to J_1, which the table already answered. It is an identity in a,
b and c rather than a fact about signs, so one line serves both signs of the
discriminant; D = 0 is the sole exclusion, and the division by D is why. The two
shapes the reduction cannot speak for get their own arms -- a zero leading
coefficient leaves a linear power, a zero discriminant leaves a power of u --
which is what lets a symbolic quadratic be answered as a piecewise instead of
carrying a division by zero.

A numerator of higher degree is divided by the quadratic first: N = qQ + r takes
one power off the denominator and two degrees off the numerator, and ends. An
improper fraction is declined rather than divided out here, since the recursion
bottoms out at a single power with a numerator of degree two or more -- exactly
the case SolveByPartialFractions already opens with a long division.

The remainder is computed as N - qQ rather than read from the division, which
reports it already divided by the divisor: x^2 over x^2 + 2 answers
(1, -2/(x^2 + 2)), and taking that as a polynomial gives a numerator that is not
one, so every polynomial-numerator case declined.

A numerator already along the denominator's derivative leaves no second integral
and the term is dropped rather than multiplied by zero. That is not tidiness:
0 * (2x/(x^2 + 1)) is `0 provided not 1 + x^2 = 0` and not 0, so multiplying
would attach a condition to x/(x^2 + 1)^2, which had none and is owed none.

A denominator with real roots now takes this route rather than partial
fractions. The value is unchanged and the form is shorter -- one logarithm where
there were two -- which is why the metric that ranks candidates selects it.

Measured, both arms on one machine, the same way. Where the integrand is
rational this is 26 to 165 times faster, because TryStandardIntegrals runs
before every search and a shape answered outright never enters the candidate
exploration: 1/(x^2 + 1)^2 goes from 2879 ms unevaluated to 111 ms answered, and
x^2/(x^2 + 2)^2 from 92,990 ms unevaluated to 565 ms answered. Where the
integrand is a transcendental function over a repeated quadratic, none of which
has an elementary antiderivative, the same search takes 3.4 to 10.8 times longer
to conclude nothing -- e^x/(x^2 + 1)^2 from 7944 ms to 85,683 ms. That is growth
in integration by parts, which those integrands reach and this rule never
matches; it is filed separately rather than bounded here, since by-parts is
machinery every integral goes through.

Full suite 9515 passed, 0 failed. 26 new tests, each answer differentiated back
and compared with the integrand.

Part of #180.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura
@Rafael-SOWNet
Rafael-SOWNet merged commit 4e302c2 into master Sep 3, 2026
31 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the repeated-quadratic-integrals branch September 3, 2026 21:11
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.

1 participant