diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 52046c744..cb3188b36 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -353,6 +353,20 @@ and 7.3.2. `x^m F(x^n)^p`, with `F` a sum holding an inverse tangent, inverse hy | `"(a+b*atanh(c*x^(3/2)))^2/x^4".ToEntity().Integrate("x")` | `integral(...)` | 330 characters | | `"(a+b*ln(c*(d+k/x^(2/3))^n))^2/x^5".ToEntity().Integrate("x")` | `integral(...)` | 2,034 characters | +### A power of x times an inverse tangent and the logarithm of its quadratic is integrated by parts + +**Answers where there were none.** `x^4 (a + b atan(c x)) (d + e ln(1 + c^2 x^2))` ran out of time, and so did three more +of Rubi's 5.3.4. It is now integrated by parts. For an odd power of x, the two remainders that have no elementary +antiderivative on their own are paired into the derivative of a product of the inverse tangent and the logarithm +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"x*(a+b*atan(c*x))*(d+pe*ln(1+c^2*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 1,742 characters | +| `"x^2*(a+b*atan(c*x))*(d+pe*ln(1+c^2*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 1,710 characters | +| `"x^3*(a+b*atan(c*x))*(d+pe*ln(1+c^2*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 3,470 characters | +| `"x^4*(a+b*atan(c*x))*(d+pe*ln(1+c^2*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 5,027 characters | + ### Zero times a power with a non-real exponent is zero where the power is defined **A value where there was NaN.** `0 b^i` simplified to NaN, and so did `b^i - b^i`. That in turn made `x b^i`'s diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 1dc32a1f6..a7c60b5d9 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -26540,6 +26540,113 @@ static void Add(Dictionary into, int k, ERational value) return answer.Nodes.Any(node => node == MathS.NaN) ? null : answer; } + /// + /// A whole power of x times F = a + b atan(c x) times G = d + e ln(1 + c^2 x^2), + /// by parts on the power: with q = c^2 and n = k + 1, the remainder is + /// (1/n) int x^n (b c G + 2 e q x F)/(1 + q x^2), and x^n/(1 + q x^2) is a + /// polynomial plus (-1/q)^m x^(n - 2m)/(1 + q x^2), m = floor(n/2). + /// + /// + /// For an odd k the two remainders over the quadratic, G/(1 + q x^2) and + /// x F/(1 + q x^2), have no elementary antiderivative apiece and come with the same + /// factor, and together they are (F G)'/(b c) -- F' = b c/(1 + q x^2), + /// G' = 2 e q x/(1 + q x^2) -- so their integral is that factor times F G. + /// Integration by parts as the rules after it apply it never pairs them, and Rubi's 5.3.4 + /// x^k (a + b atan(c x))(d + e ln(1 + c^2 x^2)), k = 1 to 4, timed out. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveAPowerOfXTimesAnArctangentAndTheLogarithmOfItsQuadratic(Entity expr, Entity.Variable x) + { + if (!Integration.AnsweringTheQuestionAsked) + return null; + var arctangents = expr.Nodes.Where(node => node is Arctanf && node.ContainsNode(x)).Distinct().ToList(); + if (arctangents.Count != 1 || arctangents[0] is not Arctanf(var argument) + || !TreeAnalyzer.TryGetPolyLinear(argument, x, out var c, out var intercept) || !Functions.PartialFractions.IsZeroAsAValue(intercept)) + return null; + var arctangent = arctangents[0]; + var q = MathS.Sqr(c).InnerSimplified; + var quadratic = 1 + q * MathS.Sqr(x); + // The logarithm of 1 + q x^2, as `ln` or `log` writes it. + var logarithms = expr.Nodes.Where(node => node is Logf(var @base, var of) && @base == MathS.e && of.ContainsNode(x) + && Functions.PartialFractions.IsZeroAsAValue((of - quadratic).InnerSimplified)).Distinct().ToList(); + if (logarithms.Count != 1) + return null; + var logarithm = logarithms[0]; + static IEnumerable Factors(Entity e) => e is Mulf(var l, var r) ? Factors(l).Concat(Factors(r)) : new[] { e }; + Entity? f = null, g = null; + var k = 0; + Entity constant = Number.Integer.One; + foreach (var factor in Factors(expr)) + { + if (!factor.ContainsNode(x)) + constant *= factor; + else if (factor == x) + k += 1; + else if (factor is Powf(var b0, Number.Integer { EInteger: var power }) && b0 == x && power.Sign > 0 && power.CanFitInInt32()) + k += power.ToInt32Unchecked(); + else if (f is null && factor.ContainsNode(arctangent) && !factor.ContainsNode(logarithm)) + f = factor; + else if (g is null && factor.ContainsNode(logarithm) && !factor.ContainsNode(arctangent)) + g = factor; + else + return null; + } + if (f is null || g is null || k > 20) + return null; + // F = a + b atan(c x) and G = d + e ln(1 + q x^2), each linear in its function. + var s = Variable.CreateUnique(expr, "s_al"); + if (f.Substitute(arctangent, s).ContainsNode(x) || g.Substitute(logarithm, s).ContainsNode(x) + || !TreeAnalyzer.TryGetPolyLinear(f.Substitute(arctangent, s), s, out var b, out _) + || !TreeAnalyzer.TryGetPolyLinear(g.Substitute(logarithm, s), s, out var e, out _) + || Functions.PartialFractions.IsZeroAsAValue(b) || Functions.PartialFractions.IsZeroAsAValue(e)) + return null; + // x^n/(1 + q x^2): the polynomial and the factor of x^(n - 2m)/(1 + q x^2). + (Entity Polynomial, Entity Remainder) Divided(int n) + { + Entity polynomial = Number.Integer.Zero; + Entity factor = Number.Integer.One; + for (var j = 0; j < n / 2; j++) + { + // x^0 written as 1: the power brings `provided not x = 0` with it. + var degree = n - 2 - 2 * j; + polynomial += factor / q * (degree == 0 ? Number.Integer.One : degree == 1 ? x : MathS.Pow(x, Number.Integer.Create(degree))); + factor = -factor / q; + } + return (polynomial, factor); + } + var n1 = k + 1; + var (p1, r1) = Divided(n1); + var (p2, r2) = Divided(n1 + 1); + // A monomial at a time: `(x^2/q - 1/q^2) atan(c x)` asked whole was declined. + Entity? Asked(Entity polynomial, Entity beside) + { + Entity sum = Number.Integer.Zero; + foreach (var term in Sumf.LinearChildren(polynomial).Select(monomial => (monomial * beside).InnerSimplified)) + { + if (Functions.PartialFractions.IsZeroAsAValue(term)) + continue; + if (Integration.ComputeAsAQuestionOfItsOwn(term, x, integrateByParts: true) is not { } integrated) + return null; + sum += integrated; + } + return sum; + } + if (Asked(p1, g) is not { } p1g || Asked(p2, f) is not { } p2f) + return null; + Entity remainders; + if (n1 % 2 == 0) + // r1 = r2: (r/n) int (b c G + 2 e q x F)/(1 + q x^2) = (r/n) F G. + remainders = r1 * f * g; + else + { + if (Asked(x, g / quadratic) is not { } xg || Asked(Number.Integer.One, f / quadratic) is not { } fq) + return null; + remainders = b * c * r1 * xg + 2 * e * q * r2 * fq; + } + var answer = (constant * (MathS.Pow(x, Number.Integer.Create(n1)) * f * g - b * c * p1g - 2 * e * q * p2f - remainders) / n1).InnerSimplified; + return answer.Nodes.Any(node => node == MathS.NaN) ? null : answer; + } + /// /// A rational function of a sine, or of a cosine, over a quadratic in it with a symbol /// among the coefficients: sin(x)/(a + b sin(x) + c sin(x)^2), and the same with diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 575b64b8b..fee1b7b9e 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -680,6 +680,9 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // A power of a sine, cosine or hyperbolic one over a binomial in a power of it, over the // binomial's roots, before the rules that spent the budget on `sinh(y)^4/(a + b sinh(y)^3)`. if ((answer = IndefiniteIntegralSolver.SolveAPowerOverABinomialInAPowerOfASineOverItsRoots(expr, x)) is { }) return answer; + // A power of x times an inverse tangent and the logarithm of its quadratic, by parts with the + // two remainders that have no antiderivative apiece paired, before the rules that timed out. + if ((answer = IndefiniteIntegralSolver.SolveAPowerOfXTimesAnArctangentAndTheLogarithmOfItsQuadratic(expr, x)) is { }) return answer; // A rational function over the quadratic of an inverse tangent beside it, split, before the // rules that timed out on `x^4 atan(a x)/(c + a^2 c x^2)`. if ((answer = IndefiniteIntegralSolver.SolveARationalFunctionOverTheQuadraticOfAnArctangentBesideIt(expr, x)) is { }) return answer; diff --git a/Sources/Tests/UnitTests/Calculus/APowerOfXTimesAnArctangentAndTheLogarithmOfItsQuadraticIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/APowerOfXTimesAnArctangentAndTheLogarithmOfItsQuadraticIntegralTest.cs new file mode 100644 index 000000000..d00b36846 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/APowerOfXTimesAnArctangentAndTheLogarithmOfItsQuadraticIntegralTest.cs @@ -0,0 +1,58 @@ +// +// 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 AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A power of x times an inverse tangent and the logarithm of its quadratic, by parts with the two + /// remainders that have no antiderivative apiece paired: Rubi's 5.3.4 + /// x^k (a + b atan(c x))(d + e ln(1 + c^2 x^2)), which timed out. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class APowerOfXTimesAnArctangentAndTheLogarithmOfItsQuadraticIntegralTest + { + private static void DifferentiatesBack(string integrand, params (string, double)[] pins) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + + var pinned = integral.Substitute("C", 0); + Entity original = integrand.ToEntity(); + foreach (var (name, value) in pins) + { + pinned = pinned.Substitute(name, value); + original = original.Substitute(name, value); + } + var derivative = pinned.Differentiate("x"); + foreach (var at in new[] { -2.3, -0.7, 0.4, 1.9 }) + { + 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-8, + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } + + [Theory] + [InlineData("x^4*(a+b*atan(c*x))*(d+k*ln(1+c^2*x^2))")] + [InlineData("x^3*(a+b*atan(c*x))*(d+k*ln(1+c^2*x^2))")] + [InlineData("x*(a+b*atan(c*x))*(d+k*ln(1+c^2*x^2))")] + [InlineData("(a+b*atan(c*x))*(d+k*ln(1+c^2*x^2))")] + public void IsAnsweredAndDifferentiatesBack(string integrand) + { + DifferentiatesBack(integrand, ("a", 1.3), ("b", 0.7), ("c", 0.3), ("d", 2), ("k", -0.4)); + DifferentiatesBack(integrand, ("a", -2), ("b", 1.5), ("c", -1.2), ("d", 0.5), ("k", 3)); + } + } +}