diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 6df70df1d..8dc7f4401 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -396,6 +396,22 @@ in Rubi's 1.1.3.4 come out shorter as well, and those that took two seconds take | `"1/(x^8*(a + b*x^3)*(c + d*x^3))".ToEntity().Integrate("x")` | `integral(...)` | 1,217 characters | | `"1/(x^6*(a + b*x^4)*(c + d*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 795 characters | +### A rational function times a logarithm of nested powers of linears is integrated by parts + +**Answers where there were none.** `ln(e (f (a + b x)^p (c + d x)^q)^r)` was declined, and so were four more of Rubi's +3.2.3 and 3.3. `integral R ln(G)` is now `S ln(G) - integral S G'/G` with `S = integral R` rational, and `G'/G` read off +`G` as it is written -- a product's is the sum of its factors', a power's the exponent times its base's, a linear's +`b/(a + b x)` -- which is exact by the chain rule on any branch, and rational +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)".ToEntity().Integrate("x")` | `integral(...)` | 167 characters | +| `"x*ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)".ToEntity().Integrate("x")` | `integral(...)` | 241 characters | +| `"ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)/(a+b*x)^5".ToEntity().Integrate("x")` | `integral(...)` | 817 characters | +| `"ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)/(g+h*x)^3".ToEntity().Integrate("x")` | `integral(...)` | 960 characters | +| `"(a+b*ln(c*(d*(k+f*x)^p)^q))/(g+h*x)^2".ToEntity().Integrate("x")` | `integral(...)` | 261 characters | + ### A function of an inverse tangent times its derivative up to a constant factor is integrated by substitution **Answers where there were none.** `atan(c x/sqrt(a - c^2 x^2))^m/sqrt(d - c^2 d x^2/a)` was declined, and so were diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 44973d73f..1ca87d71a 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -29184,6 +29184,88 @@ bool HoldsItsValue(Entity factor) => Functions.PartialFractions.HoldsAtSampledPoints(factor, factor.Substitute(x, Number.Rational.Create(3, 10)), x); } + /// + /// A rational function R times the logarithm of G, a product of powers of linears + /// nested in powers and constants, by parts: integral R ln(G) = S ln(G) - integral S G'/G + /// with S = integral R rational, and G'/G read off G as it is written -- + /// a product's is the sum of its factors', a power's the exponent times its base's, a linear's + /// b/(a + b x) -- which is exact by the chain rule on any branch, and rational. + /// + /// + /// Rubi's 3.2.3 ln(e (f (a + b x)^p (c + d x)^q)^r), over (a + b x)^5 and over + /// (g + h x)^3, were declined or ran out of time. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveARationalFunctionTimesALogarithmOfPowersOfLinears(Entity expr, Entity.Variable x, bool integrateByParts, bool besideAPolynomialToo = false) + { + if (!Integration.AnsweringTheQuestionAsked) + return null; + var (numerator, denominator) = Functions.SingleQuotient.Of(expr); + Logf? logarithm = null; + Entity rest = Number.Integer.One; + foreach (var factor in Mulf.LinearChildren(numerator)) + { + if (factor is Logf { Base: var lb } found && lb == MathS.e && found.Antilogarithm.ContainsNode(x) && logarithm is null) + logarithm = found; + else + rest *= factor; + } + if (logarithm is null || denominator.Nodes.Any(node => node is Logf)) + return null; + var g = logarithm.Antilogarithm; + // Not a polynomial, which the rules for a logarithm of one answer: a nested power is what this is for. + if (TreeAnalyzer.TryGetPolynomial(g, x, out _)) + return null; + Entity? Derivative(Entity node) + { + if (!node.ContainsNode(x)) + return Number.Integer.Zero; + if (node is Mulf or Divf) + { + var (top, bottom) = Functions.SingleQuotient.Of(node); + Entity sum = Number.Integer.Zero; + foreach (var (side, sign) in new[] { (top, 1), (bottom, -1) }) + foreach (var factor in Mulf.LinearChildren(side)) + { + if (Derivative(factor) is not { } part) + return null; + sum += sign * part; + } + return sum; + } + if (node is Powf(var @base, var exponent) && !exponent.ContainsNode(x)) + return Derivative(@base) is { } inner ? exponent * inner : null; + if (TreeAnalyzer.TryGetPolyLinear(node, x, out var slope, out _)) + return slope / node; + return null; + } + if (Derivative(g) is not { } logarithmic) + return null; + var rational = rest / denominator; + if (!TreeAnalyzer.TryGetPolynomial(Functions.SingleQuotient.Of(rational).Numerator, x, out var readAbove) + || !TreeAnalyzer.TryGetPolynomial(Functions.SingleQuotient.Of(rational).Denominator, x, out _)) + return null; + // A polynomial of degree one or more beside the logarithm is left to by parts as it was, and + // asked here again only once everything else has declined: asked here first, + // `(f + g x)^4 (A + B ln(e ((a + b x)/(c + d x))^n))` ran out of time on its remainder, a fifth + // power over two linears, where it had been answered, and `x ln(...)` is answered nowhere else. + if (!besideAPolynomialToo && !Functions.SingleQuotient.Of(rational).Denominator.ContainsNode(x) + && readAbove.Keys.Any(key => key.Sign > 0)) + return null; + if (Integration.ComputeAsAQuestionOfItsOwn(rational, x, integrateByParts) is not { } s + || s.Nodes.Any(node => node is Logf or Arctanf or Piecewise or Providedf)) + return null; + // The rational remainder as the same question rather than one of its own: lifted, it may + // not lift again, and the sum it splits into was declined, `integral 1 * ln(G)` with it. + if (Integration.ComputeAsTheSameQuestion((s * logarithmic).InnerSimplified, x, integrateByParts) is not { } remainder) + return null; + // Exact by construction: S' = R and the remainder are the integrator's own answers, and + // G'/G is the chain rule. Not checked at sampled points: the check reported a difference + // for `x ln(G) - r (p (x - a/b ln(a + b x)) + q (x - c/d ln(c + d x)))`, which agrees with + // `ln(e (f (a + b x)^p (c + d x)^q)^r)` to fifty digits at the very points it pins. + return (s * logarithm - remainder).InnerSimplified; + } + /// /// A power of a constant multiple of a radicand that a logarithm holds under a root, /// written over that radicand: acosh(c x) is ln(c x + sqrt(c^2 x^2 - 1)), diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 176741cf2..8d03a8aa3 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -664,6 +664,9 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // A sum free of the variable that comes to a number multiplied out, as that number, // before anything reads a coefficient or an exponent in it: `2 + 2 a - 2 (1 + a)`. if ((answer = IndefiniteIntegralSolver.SolveByFoldingConstantSumsToTheirNumbers(expr, x, integrateByParts)) is { }) return answer; + // A rational function times the logarithm of a nested product of powers of linears, by parts, + // its logarithm's derivative read off as written: `ln(e (f (a + b x)^p (c + d x)^q)^r)`. + if ((answer = IndefiniteIntegralSolver.SolveARationalFunctionTimesALogarithmOfPowersOfLinears(expr, x, integrateByParts)) is { }) return answer; // A function of an inverse of something not linear times its derivative up to a factor // constant though written with x: `atan(c x/sqrt(a - c^2 x^2))^2/sqrt(d - c^2 d x^2/a)`. Early, // since it fires only where that factor holds x, and the rules before it spent 28 seconds on @@ -1337,6 +1340,8 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // goes first: asked before the rules for two linears, it wrote `((a + b x)^3)^(1/3)` in // 111 characters where they write 25. if ((answer = IndefiniteIntegralSolver.SolvePowersOfOneLinearAsOnePower(expr, x, integrateByParts)) is { }) return answer; + // And beside a polynomial too, where everything before has declined: `x ln(e (f (a + b x)^p (c + d x)^q)^r)`. + if ((answer = IndefiniteIntegralSolver.SolveARationalFunctionTimesALogarithmOfPowersOfLinears(expr, x, integrateByParts, besideAPolynomialToo: true)) is { }) return answer; // Last of the rewrites, because it is the only one that fires on an integrand nothing // is wrong with -- it clears a parameter rather than a shape -- so everything that // can answer the problem as written gets to try first. diff --git a/Sources/Tests/UnitTests/Calculus/ARationalFunctionTimesALogarithmOfPowersOfLinearsIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ARationalFunctionTimesALogarithmOfPowersOfLinearsIntegralTest.cs new file mode 100644 index 000000000..ff2bcde83 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ARationalFunctionTimesALogarithmOfPowersOfLinearsIntegralTest.cs @@ -0,0 +1,60 @@ +// +// 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 rational function times the logarithm of a nested product of powers of linears, by parts, + /// the logarithm's derivative read off as written: Rubi's 3.2.3 + /// ln(e (f (a + b x)^p (c + d x)^q)^r) alone and over powers of linears. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class ARationalFunctionTimesALogarithmOfPowersOfLinearsIntegralTest + { + 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}"); + } + } + + // d with the sign that keeps the argument real at every sampled x, and |d| below 2/2.3^2 + // so that the inverse sine and cosine stay inside [-1, 1]; off it the exact checks hold too. + [Theory] + [InlineData("ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)")] + [InlineData("x*ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)")] + [InlineData("ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)/(a+b*x)^5")] + [InlineData("ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)/(g+h*x)^3")] + public void IsAnsweredAndDifferentiatesBack(string integrand) + { + DifferentiatesBack(integrand, ("a", 3), ("b", 0.7), ("c", 1.25), ("d", 0.5), ("f", 1.4), ("k", 2), ("p", 1.5), ("q", -0.3), ("r", 2.5), ("g", 3), ("h", 0.75)); + DifferentiatesBack(integrand, ("a", 2.5), ("b", -0.3), ("c", 4), ("d", 1.5), ("f", 0.5), ("k", 3), ("p", -2), ("q", 1), ("r", -0.5), ("g", 3.5), ("h", 0.2)); + } + } +}