From 4e01e9012fde3a322b6d582df1a357b490f993aa Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 30 Sep 2026 15:34:35 +0000 Subject: [PATCH] The special functions integrate by parts None of the nine special functions was a factor integration by parts would differentiate, so a power of x beside one had no route: x erf(b x), x^2 Ei(b x) and x Si(b x) were all left unevaluated, and so were Ei, li, Si, Ci, Shi and Chi of a linear argument on their own. Each has an elementary derivative, which is what puts the logarithm and the inverse trigonometric functions first in LIATE, and they stand beside them now: against a power of x the function is differentiated, and what is left is the power times e^(-u^2), e^u/u, sin(u)/u and the like, which the rules answer. Alone, each is integrated against 1, as erf already was: int Ei(u) = u Ei(u) - e^u, int li(u) = u li(u) - Ei(2 ln u), int Si(u) = u Si(u) + cos(u), int Ci(u) = u Ci(u) - sin(u), int Shi(u) = u Shi(u) - cosh(u) and int Chi(u) = u Chi(u) - sinh(u), each over the rate. After Si or Ci the remainder x^m sin(b x) needs parts again, and with more nodes than x^m Si(b x) and no factor left to differentiate, the measure that bounds the descent declined it. Against a polynomial the next steps descend on its degree, so parts are allowed on that remainder there. Allowed against anything, they took erf(b x)^2 from a decline in a third of a second to a timeout. Rubi's family 8, whose statable files are #1501's tranche: 8.1 from 9 to 105 of 177, 8.3 from 2 to 34 of 47, 8.4 from 0 to 44 of 98 and 8.5 from 18 to 75 of 98, 29 to 258 of 420 in all, with 0 wrong and 0 timeouts on master (371b38e1) and here. Families 0 to 7, side by side with master: family 0 is 1753 of 1814 on both, row for row, and the families sample goes from 810 to 811 of 912, 0 wrong on both. The move is 3.1.5:213, (a + b ln(c x^n))/(x^2 (d + e ln(f x^m))), which master declines in 5.0 s alone and this answers in 0.9 s. SpecialFunctionsByPartsTest has 24 rows from Rubi's 8.1, 8.3, 8.4 and 8.5, each differentiated back with its parameters pinned. Part of #1501. Co-Authored-By: Claude Opus 5.5 --- BREAKING-CHANGES.md | 14 +++ .../Integration/IndefiniteIntegralSolver.cs | 47 ++++++++- .../Integration/IntegralPatterns.cs | 36 +++++++ .../Calculus/SpecialFunctionsByPartsTest.cs | 99 +++++++++++++++++++ 4 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 Sources/Tests/UnitTests/Calculus/SpecialFunctionsByPartsTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index e4bc750a0..90e0f8f79 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -2178,6 +2178,20 @@ multiple of it, `(d - c^2 d x^2)^(3/2)`, is written over it the same way, with Rubi's 5.1.4, 5.1.5, 5.2.4 and 5.2.5, all 504 problems that count: 405 to 463, no row lost, 77 timeouts to 19. +### The special functions are integrated by parts, and so is what leads to one + +Integration by parts differentiates each of the nine special functions now, where it meets one +beside a power of `x`, as it does a logarithm or an inverse trigonometric function: each has an +elementary derivative. `Ei`, `li`, `Si`, `Ci`, `Shi` and `Chi` of a linear argument are integrated +alone, against 1, as `erf` already was +([#1501](https://github.com/asc-community/AngouriMath/issues/1501)). An integrand holding one of +these functions had no reading in 2.5.0, which the entries for the functions themselves record. An +elementary integrand whose antiderivative is reached through one is answered where it was not: + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"(a+b*ln(c*x^n))/(x^2*(d+e*ln(f*x^m)))".Integrate("x")`, Rubi's 3.1.5 row 213 | `integral(...)` | an antiderivative in `Ei`, provided `f > 0` and `e^d f^e > 0` | + ### An inverse trigonometric function below the bar is integrated to the sine and cosine integrals `1/arcsin(x)` was left unintegrated. Under the substitution that undoes the inverse function, diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index dfab1edd8..4b57434f7 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -1595,6 +1595,13 @@ private static Entity IntegrateAPowerOfTheVariable(Entity @base, Entity power, E /// gives an antiderivative holding x*atan(x) again, which is where the search went /// instead and why it was left unevaluated. /// + /// + /// The special functions belong here for the same reason, since each has an elementary + /// derivative: x Ei(b x) leaves x e^(b x)/2 after one step, and + /// x^2 Si(b x) leaves x^2 sin(b x)/3, where integrating the special function + /// first gives an antiderivative that holds it again. + /// https://github.com/asc-community/AngouriMath/issues/1501 + /// /// https://github.com/asc-community/AngouriMath/issues/718 /// private static bool IsDifferentiatedBeforeAPolynomial(Entity factor) @@ -1603,6 +1610,8 @@ private static bool IsDifferentiatedBeforeAPolynomial(Entity factor) Logf or Entity.Arcsinf or Entity.Arccosf or Entity.Arctanf or Entity.Arccotanf or Entity.Arcsecantf or Entity.Arccosecantf => true, + Entity.Erff or Entity.Erfcf or Entity.Erfif or Entity.Eif or Entity.Lif + or Entity.Sif or Entity.Cif or Entity.Shif or Entity.Chif => true, // And a whole power of one, which is the same function for this purpose: // differentiating ln(x)^2 gives 2ln(x)/x, whose x cancels against the integrated // polynomial exactly as ln(x)'s does, leaving x*ln(x) -- one step simpler, and @@ -1616,6 +1625,31 @@ or Entity.Arctanf or Entity.Arccotanf _ => false }; + /// + /// Whether is a special function of an argument linear in + /// , other than the logarithmic integral: differentiated beside a + /// power of x, it leaves that power times sin(u), e^u or the like, + /// which parts against the power answer in as many steps as its degree. + /// + /// + /// Not li: its derivative 1/ln(u) leaves x^m/ln(a + b x), which the + /// exponential integral's rule answers, and parts there would integrate the reciprocal of + /// the logarithm back into li. + /// + private static bool IsASpecialFunctionOfALinear(Entity factor, Variable x) + => (factor switch + { + Entity.Erff(var u) => u, + Entity.Erfcf(var u) => u, + Entity.Erfif(var u) => u, + Entity.Eif(var u) => u, + Entity.Sif(var u) => u, + Entity.Cif(var u) => u, + Entity.Shif(var u) => u, + Entity.Chif(var u) => u, + _ => null + }) is { } argument && TreeAnalyzer.TryGetPolyLinear(argument, x, out _, out _); + internal static Entity? SolveIntegratingByParts(Entity expr, Entity.Variable x) { // The measure the nested call below decreases on. Read once, since every step @@ -1735,8 +1769,19 @@ static Entity WithTheConstantMatchedTo(Entity antiderivative, Entity derivativeO // is the runaway the node bound exists for: measured at thirty seconds for a // decline, where the node bound alone declined it in one. var remainingPower = HighestDifferentiatedPower(remaining); + // And after a special function of a linear argument, whose derivative leaves the + // power of x beside sin(u), e^u and the like: `x Si(b x)` leaves `x sin(b x)/2`, + // which parts against the power answer in as many steps as its degree. It has + // more nodes than `x Si(b x)` and no factor left to differentiate, so the measure + // alone declined it, and with it every power of x beside Si or Ci. + // + // Only against a polynomial, which is what makes the next steps a descent on its + // degree. Allowed against anything, it took erf(b x)^2, whose second step is + // against x e^(-b^2 x^2), from a decline in a third of a second to a timeout. + // https://github.com/asc-community/AngouriMath/issues/1501 var partsOnTheRemainder = remaining.Nodes.Count() < wholeSize - || (remainingPower >= 1 && remainingPower < wholePower); + || (remainingPower >= 1 && remainingPower < wholePower) + || IsASpecialFunctionOfALinear(v, x) && MathS.TryPolynomial(u, x, out _); // Spelled as the product its own next step reads, where there is one. `Simplify` // writes `2 arctan(x)/(1 + x^2) * x^2/2` as `arctan(x) x^2/(x^2 + 1)`, a // quotient, and this rule runs on a product. `x*arctan(x)^2` is answered by diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs index 08dbf25d6..cf2e5a968 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs @@ -61,6 +61,12 @@ internal static Entity AntiderivativeLog(Entity arg) Entity.Erff(var arg) => arg, Entity.Erfcf(var arg) => arg, Entity.Erfif(var arg) => arg, + Entity.Eif(var arg) => arg, + Entity.Lif(var arg) => arg, + Entity.Sif(var arg) => arg, + Entity.Cif(var arg) => arg, + Entity.Shif(var arg) => arg, + Entity.Chif(var arg) => arg, Entity.Arcsinf(var arg) => arg, Entity.Arccosf(var arg) => arg, Entity.Arctanf(var arg) => arg, @@ -344,6 +350,36 @@ _ when GaussianMoment(expr, x) is { } moment => moment, TreeAnalyzer.TryGetPolyLinear(arg, x, out var a, out _) => (arg * MathS.Erfi(arg) - MathS.Pow(MathS.e, MathS.Sqr(arg)) / MathS.Sqrt(MathS.pi)) / a, + // And the exponential, logarithmic, trigonometric and hyperbolic integrals, by parts + // against 1 the same way: each derivative is elementary and cancels the u the + // integrated 1 leaves beside it. int Ei(u) = u Ei(u) - e^u, int li(u) = u li(u) - + // Ei(2 ln u), int Si(u) = u Si(u) + cos(u), int Ci(u) = u Ci(u) - sin(u), + // int Shi(u) = u Shi(u) - cosh(u) and int Chi(u) = u Chi(u) - sinh(u). + // https://github.com/asc-community/AngouriMath/issues/1501 + Entity.Eif(var arg) when + TreeAnalyzer.TryGetPolyLinear(arg, x, out var a, out _) => + (arg * MathS.Ei(arg) - MathS.Pow(MathS.e, arg)) / a, + + Entity.Lif(var arg) when + TreeAnalyzer.TryGetPolyLinear(arg, x, out var a, out _) => + (arg * MathS.Li(arg) - MathS.Ei(2 * MathS.Ln(arg))) / a, + + Entity.Sif(var arg) when + TreeAnalyzer.TryGetPolyLinear(arg, x, out var a, out _) => + (arg * MathS.Si(arg) + MathS.Cos(arg)) / a, + + Entity.Cif(var arg) when + TreeAnalyzer.TryGetPolyLinear(arg, x, out var a, out _) => + (arg * MathS.Ci(arg) - MathS.Sin(arg)) / a, + + Entity.Shif(var arg) when + TreeAnalyzer.TryGetPolyLinear(arg, x, out var a, out _) => + (arg * MathS.Shi(arg) - MathS.Hyperbolic.Cosh(arg)) / a, + + Entity.Chif(var arg) when + TreeAnalyzer.TryGetPolyLinear(arg, x, out var a, out _) => + (arg * MathS.Chi(arg) - MathS.Hyperbolic.Sinh(arg)) / a, + Entity.Absf(var arg) when TreeAnalyzer.TryGetPolyLinear(arg, x, out var a, out _) => // ∫ |ax + b| dx = sgn(ax + b) * (ax + b)^2 / (2a) MathS.Signum(arg) * MathS.Pow(arg, 2) / (2 * a), diff --git a/Sources/Tests/UnitTests/Calculus/SpecialFunctionsByPartsTest.cs b/Sources/Tests/UnitTests/Calculus/SpecialFunctionsByPartsTest.cs new file mode 100644 index 000000000..b9ead215c --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/SpecialFunctionsByPartsTest.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using System; +using System.Linq; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// The special functions integrated by parts: each has an elementary derivative, so against a + /// power of x it is the factor differentiated, and alone it is integrated against 1. + /// The rows are Rubi's, from its files 8.1, 8.3, 8.4 and 8.5. + /// https://github.com/asc-community/AngouriMath/issues/1501 + /// + [Trait("Area", "Calculus")] + public sealed class SpecialFunctionsByPartsTest + { + /// Off 0, where the negative powers are undefined. + private static readonly double[] Points = { -1.7, -0.6, 0.35, 0.9, 1.45 }; + + /// + /// Integrates, pins the parameters, and compares the derivative of the answer with the + /// integrand at . The parameters are pinned after integrating, so the + /// rule is asked the symbolic question. + /// + private static void DifferentiatesBack(string integrand, params (string Name, string Value)[] pins) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Entity Pinned(Entity e) => pins.Aggregate(e, (current, pin) => current.Substitute(pin.Name, pin.Value.ToEntity())); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in Points) + { + var got = derivative.Substitute("x", at).EvalNumerical(); + var want = original.Substitute("x", at).EvalNumerical(); + var difference = Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart); + var scale = Math.Max(1.0, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)); + Assert.True(difference / scale < 1e-9, + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } + + private static readonly (string, string)[] Parameters = { ("a", "2/5"), ("b", "13/10"), ("c", "7/10"), ("d", "19/10") }; + + /// + /// Alone, against 1: int Ei(u) = u Ei(u) - e^u and its kin, each over the rate. + /// + [Theory] + [InlineData("Ei(b*x)")] + [InlineData("Ei(a + b*x)")] + [InlineData("Si(b*x)")] + [InlineData("Si(a + b*x)")] + [InlineData("Ci(b*x)")] + [InlineData("Shi(a + b*x)")] + [InlineData("Chi(b*x)")] + public void ASpecialFunctionAloneIsByPartsAgainstOne(string integrand) + => DifferentiatesBack(integrand, Parameters); + + /// + /// The logarithmic integral, where its argument stays above 0 at every point: a = 3 + /// keeps a + b x between 0.79 and 4.9. + /// + [Theory] + [InlineData("li(a + b*x)")] + [InlineData("x*li(a + b*x)")] + public void TheLogarithmicIntegralIsByParts(string integrand) + => DifferentiatesBack(integrand, ("a", "3"), ("b", "13/10")); + + /// + /// Against a power of x, the special function is the factor differentiated: what is + /// left is the power times e^(-u^2), e^u/u, sin(u)/u and the like. + /// + [Theory] + [InlineData("x^3*erf(b*x)")] + [InlineData("x^2*erf(b*x)")] + [InlineData("x*erf(b*x)")] + [InlineData("erf(b*x)/x^2")] + [InlineData("erf(b*x)/x^3")] + [InlineData("(c + d*x)^2*erfc(a + b*x)")] + [InlineData("x*erfi(b*x)")] + [InlineData("x^2*Ei(b*x)")] + [InlineData("x*Ei(a + b*x)")] + [InlineData("Ei(b*x)/x^2")] + [InlineData("x*Si(b*x)")] + [InlineData("x^2*Ci(b*x)")] + [InlineData("Si(b*x)/x^2")] + [InlineData("x*Shi(b*x)")] + [InlineData("x^3*Chi(b*x)")] + public void APowerTimesASpecialFunctionIsByParts(string integrand) + => DifferentiatesBack(integrand, Parameters); + } +}