diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 22579126a..2d782b045 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 | +### Powers of multiples of powers of one linear are gathered into one power of it + +**Answers where there were none.** `1/(sqrt(-2 - b x) sqrt(2 + b x))` was declined, and so were four more of Rubi's +1.1.1.2, 1.1.1.3, 1.2.2.2 and 1.3.1. A power `B^p` that is not whole, of a polynomial `B` that is a constant times +`L^k` for one linear `L`, is now written `(B^p/L^(k p)) L^(k p)`, and the powers of one linear are integrated as one. +The quotient is constant on each interval where it is continuous, so it stays in the answer as written +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/(sqrt(-2-b*x)*sqrt(2+b*x))".ToEntity().Integrate("x")` | `integral(...)` | 73 characters | +| `"sqrt(a+b*x)/(x*sqrt(-a-b*x))".ToEntity().Integrate("x")` | `integral(...)` | 56 characters | +| `"(a+b*x)^n/(x*(-a-b*x)^n)".ToEntity().Integrate("x")` | `integral(...)` | 52 characters | +| `"(a^3+3*a^2*b*x+3*a*b^2*x^2+b^3*x^3)^p".ToEntity().Integrate("x")` | `integral(...)` | 187 characters | +| `"1/sqrt(2+2*a-2*(1+a)+c*x^4)".ToEntity().Integrate("x")` | `integral(...)` | 119 characters | + ### A linear below the cube root of a quadratic is integrated in closed form **Answers where there were none.** `1/((d + e x) (d^2 + 3 e^2 x^2)^(1/3))` was declined, and so were six more of Rubi's diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index d7dab662e..2a6e908e1 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -20065,6 +20065,96 @@ Piecewise piecewise when piecewise.Cases.Any() => piecewise.Cases.First().Expres return Functions.PartialFractions.DerivativeHoldsAtSampledPoints(answer, expr, x) ? answer : null; } + /// + /// Powers that are not whole of polynomials that are constant multiples of powers of one + /// linear, gathered into one power of it: B^p = (B^p/L^(k p)) L^(k p) for + /// B = lambda L^k, so sqrt(-2 - b x) sqrt(2 + b x) is a constant times + /// 2 + b x and (a^3 + 3 a^2 b x + 3 a b^2 x^2 + b^3 x^3)^p one times + /// (a + b x)^(3 p). + /// + /// + /// B^p/L^(k p) is constant wherever it is continuous: its logarithmic derivative is + /// p B'/B - k p L'/L, which is zero for B = lambda L^k. It is kept in the + /// answer as written. Rubi's 1.1.1.2 1/(sqrt(-2 - b x) sqrt(2 + b x)), 1.1.1.3 + /// (a + b x)^n/(x (-a - b x)^n) and 1.3.1 (a^3 + 3 a^2 b x + 3 a b^2 x^2 + b^3 x^3)^p + /// were declined. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolvePowersOfOneLinearAsOnePower(Entity expr, Entity.Variable x, bool integrateByParts) + { + if (!Integration.AnsweringTheQuestionAsked) + return null; + // Without a leading `1 *`, which is another spelling to the rules that read the result. + static Entity Times(Entity a, Entity b) => a == Number.Integer.One ? b : a * b; + static bool IsWhole(Entity power) => power is Number.Integer; + // The root r and degree k of a polynomial that is c (x - r)^k, or null. + (Entity Root, int Degree)? AsAPowerOfALinear(Entity polynomial) + { + if (!TreeAnalyzer.TryGetPolynomial(polynomial, x, out var read) || read.Keys.Any(key => key.Sign < 0)) + return null; + var present = read.Where(term => !VanishesIdentically(term.Value.Expand())).ToList(); + if (present.Count == 0 || present.Max(term => term.Key) is not { } top || top.Sign <= 0 || !top.CanFitInInt32()) + return null; + var k = top.ToInt32Unchecked(); + var lead = read[top]; + var next = read.TryGetValue(top - EInteger.One, out var n0) ? n0 : Number.Integer.Zero; + var root = (-next / (k * lead)).InnerSimplified; + if (k > 1 && !VanishesIdentically((polynomial - lead * MathS.Pow(x - root, Number.Integer.Create(k))).Expand())) + return null; + return (root, k); + } + var (numerator, denominator) = Functions.SingleQuotient.Of(expr); + var members = new List<(Entity Base, Entity Power, int Degree, Entity Root)>(); + Entity rest = Number.Integer.One; + foreach (var (side, below) in new[] { (numerator, false), (denominator, true) }) + foreach (var factor in Mulf.LinearChildren(side)) + { + if (factor is Powf(var @base, var power) && !IsWhole(power) && !power.ContainsNode(x) + && AsAPowerOfALinear(@base) is var (root, degree)) + members.Add((@base, below ? -power : power, degree, root)); + else + rest = below ? rest / factor : Times(rest, factor); + } + // Gathered by root; worth doing where a group has two members or a power of a linear. + var groups = new List>(); + foreach (var member in members) + { + if (groups.FirstOrDefault(group => VanishesIdentically((group[0].Root - member.Root).Expand().InnerSimplified)) is { } group) + group.Add(member); + else + groups.Add(new() { member }); + } + if (!groups.Any(group => group.Count > 1 || group.Any(member => member.Degree > 1))) + return null; + Entity constant = Number.Integer.One; + Entity gathered = rest; + foreach (var group in groups) + { + var linear = group.FirstOrDefault(member => member.Degree == 1).Base ?? x - group[0].Root; + Entity total = Number.Integer.Zero; + foreach (var member in group) + { + var exponent = member.Degree * member.Power; + // The linear itself is its own power, with nothing to carry in front. + if (member.Base != linear) + constant = Times(constant, MathS.Pow(member.Base, member.Power) / MathS.Pow(linear, exponent)); + total += exponent; + } + total = total.InnerSimplified; + if (VanishesIdentically(total.Expand())) + continue; + // Spelled as a quotient where the power is negative, as the rules read it. + gathered = total is Number.Real { IsNegative: true } || (total is Mulf(Number.Real { IsNegative: true }, _)) + ? gathered / MathS.Pow(linear, (-total).InnerSimplified) + : Times(gathered, MathS.Pow(linear, total)); + } + if (Integration.ComputeAsAQuestionOfItsOwn(gathered, x, integrateByParts) is not { } joined) + return null; + if (!Functions.PartialFractions.DerivativeHoldsAtSampledPoints(joined, gathered, x)) + return null; + return constant * joined; + } + /// /// A power of a linear that is not whole, times a linear, over a whole power of a quadratic, /// with the power of the quadratic lowered one at a time: diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index dc25a94de..820ef3d59 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -1319,6 +1319,11 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // function of x and of sin(x) and cos(x) together. `x^2/(x cos(x) - sin(x))^2` is // `((x sin(x) + cos(x))/(x cos(x) - sin(x)))'`. if ((answer = IndefiniteIntegralSolver.SolveByTrigonometricTowerAnsatz(expr, x)) is { }) return answer; + // Powers of polynomials that are multiples of powers of one linear, as one power of it, + // the quotient constant in front. Late, so that a rule answering the powers as written + // 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; // 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/PowersOfOneLinearAsOnePowerIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/PowersOfOneLinearAsOnePowerIntegralTest.cs new file mode 100644 index 000000000..290e201ce --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/PowersOfOneLinearAsOnePowerIntegralTest.cs @@ -0,0 +1,71 @@ +// +// 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 +{ + /// + /// Powers that are not whole of polynomials that are constant multiples of powers of one + /// linear, gathered into one power of it with the constant quotient in front: Rubi's 1.1.1.2 + /// 1/(sqrt(-2 - b x) sqrt(2 + b x)), 1.1.1.3 (a + b x)^n/(x (-a - b x)^n) and 1.3.1 + /// (a^3 + 3 a^2 b x + 3 a b^2 x^2 + b^3 x^3)^p. Sampled on both sides of the linear's root. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class PowersOfOneLinearAsOnePowerIntegralTest + { + 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("1/(sqrt(-2-b*x)*sqrt(2+b*x))")] + [InlineData("sqrt(a+b*x)/(x*sqrt(-a-b*x))")] + [InlineData("(a+b*x)^n/(x*(-a-b*x)^n)")] + [InlineData("(a^3+3*a^2*b*x+3*a*b^2*x^2+b^3*x^3)^n")] + public void WithSymbolsIsAnsweredAndDifferentiatesBack(string integrand) + { + DifferentiatesBack(integrand, ("a", 0.6), ("b", 1.5), ("n", 1.0 / 3)); + DifferentiatesBack(integrand, ("a", -0.75), ("b", -0.7), ("n", 2.3)); + } + + [Theory] + [InlineData("((a+b*x)^3)^(1/3)", 40)] + [InlineData("x*(-a-b*x)^(3/2)/(a+b*x)^(3/2)", 80)] + public void LeavesWhatTheRulesBeforeItAnswer(string integrand, int length) + { + // Asked before them, the rule wrote these in 111 and 106 characters, where they write + // 25 and 61. + var integral = integrand.ToEntity().Integrate("x").Stringize(); + Assert.DoesNotContain("integral(", integral); + Assert.True(integral.Length < length, integral); + } + } +}