Repository navigation
Decompose a rational function over its denominator's factors, not only its roots (#919) - #926
Merged
Conversation
…y its roots (#919) A partial fraction decomposition split N/D at a rational root of D, which was all the decomposition there was. A denominator that factors over Q with no rational root anywhere in it was left whole and its integral came back unevaluated -- even where every factor was one the integrator already reads: 1/(x^4 + 3x^2 + 2) unevaluated -> arctan(x) - sqrt(2)*arctan(sqrt(2)*x/2)/2 + C x/(x^4 + 3x^2 + 2) unevaluated -> (ln|x^2 + 1| - ln|x^2 + 2|)/2 + C 1/(x^4 + 4) unevaluated -> the antiderivative over its two quadratic factors The denominator of the first is (x^2 + 1)(x^2 + 2) and of the third (x^2 - 2x + 2)(x^2 + 2x + 2); the rule for a linear numerator over a quadratic answers both halves of each. Nothing was missing but the split. #918 supplied the factorisation over Q that makes it available, so this is the second consumer of the polynomial layer after the equation solver. The step is a coprime split, the same shape as the step at a root: one irreducible factor with its multiplicity against the product of the rest, U*A + V*B = 1 from the extended Euclidean algorithm in Q[x] -- which did not exist and is the new RationalPolynomial -- and N/(A*B) = N*V/A + N*U/B with each numerator reduced modulo its own denominator. The polynomial parts that come off cannot survive, a proper fraction less two proper fractions being a polynomial that vanishes at infinity. Both sides are strictly smaller problems of the same kind, so the integrator recurses and reaches the full decomposition whichever factor is peeled first. No condition is attached, and that is a statement rather than an omission. A and B being coprime, A*B vanishes exactly where one of them does, so the two sides are undefined at the same points; a decomposition loses a singularity when it cancels a shared factor, and this cancels nothing. The identity is checked -- overA*B + overB*A against the numerator, over Q -- rather than trusted, so the failure that survives is a refusal and not a wrong antiderivative. The decomposition is produced only where every factor is a shape an integration rule reads: linear at any multiplicity, quadratic at the first, nothing else. That costs no answer, since a piece with no rule leaves the whole integral unevaluated either way, and it is what keeps declining cheap. Deciding it by trying instead made (1 - x^4)/(1 + x^4 + x^8), whose factorisation holds the irreducible quartic x^4 - x^2 + 1, take 18s to return the same unevaluated integral it returns in 203ms, because every half of every split is a fresh problem the whole integrator then searches. Read off the factorisation, which is already in hand, the cost of declining is that one factorisation. One test changed rather than being added to. ADenominatorWithNoRationalRootIsStillDeclined pinned 1/(x^4 + 4) as out of reach on the grounds that it has no rational root; it is reducible over Q and is now answered, so the boundary it records has moved to what does not factor over Q at all. x^2/(x^4 + 1) stays, x^4 + 1 being irreducible, and 1/(x^4 + 2x^2 + 1) joins it as the power of a single irreducible. Measured on this branch against a stock build of master, same machine: unit suite 6960 passed, 0 failed F# wrapper 130/130 casbench 116/119, 0 wrong 0 error 0 timeout, equal to master propcheck 1340 checks, 0 failures rootcheck 596/596 clean simpsweep 10463/10463 agree crashcheck 1652, 0 crashed intbench families=0 46/191 -> 47/191 solved, 0 wrong, 6 timeout either side intbench families=1 30/228 both, and identical to master problem by problem #919 Co-authored-by: Claude Opus 5 <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.
Closes #919.
A partial fraction decomposition split
N/Dat a rational root ofD, which was all the decomposition there was. A denominator that factors over ℚ with no rational root anywhere in it was left whole and its integral came back unevaluated — even where every factor was one the integrator already reads.x^4 + 3x^2 + 2is(x^2 + 1)(x^2 + 2), and the rule for a linear numerator over a quadratic answers both halves. Nothing was missing but the split. #918 supplied the factorisation over ℚ that makes it available, so this is the second consumer of the polynomial layer after the equation solver.What it does
The same shape as the step at a root: one irreducible factor with its multiplicity against the product of the rest,
U*A + V*B = 1from the extended Euclidean algorithm in ℚ[x], andN/(A*B) = N*V/A + N*U/Bwith each numerator reduced modulo its own denominator. Both sides are strictly smaller problems of the same kind, so the integrator recurses and reaches the full decomposition whichever factor is peeled first.The extended Euclid did not exist — there was one over 𝔽ₚ inside the factoriser and
IntegerPolynomial.Gcdover ℤ without the cofactors — soRationalPolynomialis new: dense, lowest power first, coefficients in lowest terms as they are written.No condition is attached, and that is a statement rather than an omission.
AandBbeing coprime,A*Bvanishes exactly where one of them does, so the two sides are undefined at the same points. A decomposition loses a singularity when it cancels a shared factor; this cancels nothing. The identity is checked —overA*B + overB*Aagainst the numerator, over ℚ — rather than trusted, so what survives a failure is a refusal and never a wrong antiderivative.The guard, which is the part with a measurement behind it
The decomposition is produced only where every factor is a shape an integration rule reads: linear at any multiplicity, quadratic at the first, nothing else. That costs no answer — a piece with no rule leaves the whole integral unevaluated either way — and it is what keeps declining cheap.
Deciding it by trying instead was measured and is much worse.
(1 - x^4)/(1 + x^4 + x^8), whose factorisation holds the irreducible quarticx^4 - x^2 + 1, took 18s to return the same unevaluated integral it returns in 203ms, because every half of every split is a fresh problem the whole integrator then searches.intbenchcaught this before the guard existed: four problems crossed the 3s budget and one previously-answered problem went with them.What is still declined
x^2/(x^4 + 1)— irreducible over ℚ, and only factors once real coefficients are allowed — and1/(x^4 + 2x^2 + 1), the power of a single irreducible. The ladder that would decompose the second is deliberately not built: every term it produces is over(x^2 + c)^k, which nothing reads, so it would end in the same unevaluated integral it started from.One test changed rather than added to
ADenominatorWithNoRationalRootIsStillDeclinedpinned1/(x^4 + 4)as out of reach on the grounds that it has no rational root. It is reducible over ℚ —(x^2 - 2x + 2)(x^2 + 2x + 2)— and is now answered, so the boundary that test records has moved to what does not factor over ℚ at all.BREAKING-CHANGES.mdcarries the entry.Measured
On this branch against a stock build of
master, same machine.casbenchpropcheckrootchecksimpsweepcrashcheckintbenchfamilies=0intbenchfamilies=1Every antiderivative in the new tests is checked by differentiating it back and comparing to the integrand numerically, never against a written form.
🤖 Generated with Claude Code