From 51a3acc674cf7e892c108d097fb9556e94f78b13 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 1 Oct 2026 04:29:33 +0000 Subject: [PATCH] A constant of integration is matched to a linear divisor with a symbol in it By parts against a polynomial chooses the polynomial's antiderivative so that what the other factor's derivative divides by divides it too: against x ln(1 + x) the antiderivative of x is (x^2 - 1)/2, and the remainder is a polynomial. That was done where the division left a number over, and a + b x leaves a symbol: x^2/2 over a + b x leaves a^2/(2 b^2), so x Shi(a + b x)^2 kept x^2/(a + b x) in its remainder and nothing read it. The antiderivative less its value at the root, -a/b, is divisible by the linear, and it is written now as the linear times the quotient, so that the linear cancels as written: x Shi(a + b x)^2 and x Ei(a + b x)^2 are two rounds of parts each. Measured on the Rubi corpus, master at 6c94652f and this change on it, run side by side: family 8 373 -> 376 of 420 (x Ei, x Shi and x Chi of a + b x, squared); family 2 544 of 650, the independent suites 1756 of 1814 and families 1 and 3 to 7 at five a file 800 of 897, on both. 0 wrong everywhere. Run alone, the three are 0 of 3 on master and 3 of 3 here; the fourth problem that moved, 2.3 #622, is 22 s unevaluated on both. The unit tests pass, 14,259. The performance gate passes on d480366e, which is this change on 6c94652f: allocation is what the baseline says on all 19 gated benchmarks. SpecialFunctionsByPartsTest has two rows with a shifted argument, each differentiated back with its parameters pinned. Part of #1501. Co-Authored-By: Claude Opus 5.5 --- BREAKING-CHANGES.md | 20 +++++++++++++++++++ .../Integration/IndefiniteIntegralSolver.cs | 13 ++++++++++++ .../Calculus/SpecialFunctionsByPartsTest.cs | 11 ++++++++++ 3 files changed, 44 insertions(+) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 430aa694f..5e39bfc78 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -2315,6 +2315,26 @@ no integrand without one changes: Rubi's independent suites and a sample of its | `"Ei(b*x)^2".Integrate("x")` | `Ei * (b * x) ^ 3 / 3 / b + C`, with `Ei` a variable | an antiderivative in `Ei(b x)` and `Ei(2 b x)` | | `"x*erf(b*x)^2".Integrate("x")` | `UnrecognizedFunctionParseException`: there is no function `erf` | an antiderivative in `erf(b x)` | +### A constant of integration is matched to a linear divisor with a symbol in it + +By parts against a polynomial chooses the polynomial's antiderivative so that what the other +factor's derivative divides by divides it too, and that was done only where the division left a +number over. `a + b x` leaves a symbol: `x^2/2` over `a + b x` leaves `a^2/(2 b^2)`, so the +remainder of `x Shi(a + b x)^2` kept `x^2/(a + b x)` and nothing read it. The antiderivative less +its value at `-a/b` is taken now, written as `a + b x` times the quotient, so that the linear +cancels in the remainder ([#1501](https://github.com/asc-community/AngouriMath/issues/1501)). +`x Shi(a + b x)^2`, `x Chi(a + b x)^2` and `x Ei(a + b x)^2` are answered. An integrand holding one of +these functions had no reading in 2.5.0, which the entries for the functions themselves record, and +no integrand without one changes: Rubi's independent suites and a sample of its families 1 to 7 +are answered alone as they were. Where by parts already answered a special function of `a + b x` +beside a power of `x`, the answer is written with the same constant: `x Ei(a + b x)` now reads +`Ei(a + b x) (a + b x)(x/(2b) - a/(2b^2)) - ...`, the same function as before. + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"x*Shi(a+b*x)^2".Integrate("x")` | `a ^ 2 * Shi * x ^ 2 / 2 + a * b * 2 * Shi * x ^ 3 / 3 + b ^ 2 * Shi * x ^ 4 / 4 + C`, with `Shi` a variable | an antiderivative in `Shi(a + b x)` and `Ei(2 (a + b x))`, `Ei(-2 (a + b x))` | +| `"x*Ei(a+b*x)^2".Integrate("x")` | `a ^ 2 * Ei * x ^ 2 / 2 + a * b * 2 * Ei * x ^ 3 / 3 + b ^ 2 * Ei * x ^ 4 / 4 + C`, with `Ei` a variable | an antiderivative in `Ei(a + b x)` and `Ei(2 (a + b x))` | + ### An exponential or a hyperbolic function over several linears is split into partial fractions over them `e^x/(x (x + 1))` was left unevaluated, where `e^x/x` and `e^x/(x + 1)` were each answered with diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 90ed4521c..8694cc5d6 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -1691,6 +1691,19 @@ static Entity WithTheConstantMatchedTo(Entity antiderivative, Entity derivativeO && Functions.SingleQuotient.Of(leftOver) is var (leftOverTop, _) && !leftOverTop.ContainsNode(x) && leftOverTop.Evaled is Number.Complex { IsZero: false } constant) return (antiderivative + (-constant).Evaled).InnerSimplified; + // A linear divisor with a symbol in it, `a + b x`, leaves a symbol over, which the + // case above does not read: the antiderivative less its value at the root, -a/b, is + // divisible by the linear, and it is written as the linear times the quotient, so + // that the linear the derivative divides by cancels as written. `x Shi(a + b x)^2` + // is two rounds of parts that way, where with `x^2/2` the remainder kept + // `x^2/(a + b x)` and nothing read it. + // https://github.com/asc-community/AngouriMath/issues/1501 + if (TreeAnalyzer.TryGetPolyLinear(divisors[0], x, out var slope, out var offset) && !TreeAnalyzer.IsZero(slope) + && antiderivative.Substitute(x, (-offset / slope).InnerSimplified).InnerSimplified is var atTheRoot + && !atTheRoot.ContainsNode(x) && !TreeAnalyzer.IsZero(atTheRoot) + && TreeAnalyzer.PolynomialLongDivision((antiderivative - atTheRoot).Expand().InnerSimplified, divisors[0], genericCase: true, inTermsOf: x) is var (quotient, _) + && !quotient.ContainsNode(MathS.NaN)) + return divisors[0] * Functions.PartialFractions.Bare(quotient); return antiderivative; } diff --git a/Sources/Tests/UnitTests/Calculus/SpecialFunctionsByPartsTest.cs b/Sources/Tests/UnitTests/Calculus/SpecialFunctionsByPartsTest.cs index 0b774a01e..6df826679 100644 --- a/Sources/Tests/UnitTests/Calculus/SpecialFunctionsByPartsTest.cs +++ b/Sources/Tests/UnitTests/Calculus/SpecialFunctionsByPartsTest.cs @@ -133,5 +133,16 @@ public void BesideAPowerAndTheElementaryFactorOfItsDerivative(string integrand) [InlineData("Chi(b*x)^2")] public void ASquareIsTwoRoundsOfParts(string integrand) => DifferentiatesBack(integrand, Parameters); + + /// + /// Of a + b x, the first round against x takes x^2/2 less its value at + /// -a/b, written as a + b x times the quotient, so the linear the derivative + /// divides by cancels and what is left is the case above. + /// + [Theory] + [InlineData("x*Shi(a + b*x)^2")] + [InlineData("x*Ei(a + b*x)^2")] + public void ASquareOfAShiftedArgumentIsTwoRoundsOfParts(string integrand) + => DifferentiatesBack(integrand, Parameters); } }