diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 5b1e84346..433a7875c 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -447,6 +447,23 @@ case is most of its length ([#718](https://github.com/asc-community/AngouriMath/ | `"x^4/((a + b*x^2 + c*x^4)*sqrt(d + k*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 28,551 characters | | `"1/(x^2*(a + b*x^2 + c*x^4)*sqrt(d + k*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 10,194 characters | +### Two linears to one power are integrated as one quadratic to that power + +**Answers where there were none.** `(2 + 3 x)^4/((1 - 2 x)^(5/2) (3 + 5 x)^(5/2))` ran past any budget, and +`1/((1 - x)^(1/3) (2 - x)^(1/3) x)` was declined. `L1^p L2^p` for linears with numbers for coefficients and `p` not +whole, other than `1/2`, is now integrated as `(L1^p L2^p/Q^p)` times the integral over `Q^p`, `Q = L1 L2`. That +quotient is constant on each interval between the roots, so it stays in the answer as written +([#718](https://github.com/asc-community/AngouriMath/issues/718)). `e` is Euler's number below. + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/((1-x)^(1/3)*(2-x)^(1/3)*x)".ToEntity().Integrate("x")` | `integral(...)` | 381 characters | +| `"(e*x)^(3/2)/((1-x)^(1/4)*(1+x)^(1/4))".ToEntity().Integrate("x")` | `integral(...)` | 1,403 characters | +| `"1/((1-x)^(1/4)*(e*x)^(1/2)*(1+x)^(1/4))".ToEntity().Integrate("x")` | `integral(...)` | 1,226 characters | +| `"(2+3*x)^6/((1-2*x)^(5/2)*(3+5*x)^(5/2))".ToEntity().Integrate("x")` | `integral(...)` | 1,236 characters | +| `"(2+3*x)^4/((1-2*x)^(5/2)*(3+5*x)^(5/2))".ToEntity().Integrate("x")` | `integral(...)` | 784 characters | +| `"1/((1-2*x)^(5/2)*(2+3*x)^2*(3+5*x)^(5/2))".ToEntity().Integrate("x")` | `integral(...)` | 382 characters | + ### A power of an even quadratic over a binomial in it is split over the binomial's roots **Answers where there were none.** `(1 - x^2)/(a + b (1 - x^2)^4)` was declined. `w^m/(a + b w^n)` for `w = p + q x^2` and diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index dec1de4e6..2be6bf3a8 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -19792,6 +19792,76 @@ Piecewise piecewise when piecewise.Cases.Any() => piecewise.Cases.First().Expres return Functions.PartialFractions.DerivativeHoldsAtSampledPoints(answer, expr, x) ? answer : null; } + /// + /// Two linears to one power that is not whole, as one quadratic: + /// integral g L1^p L2^p = (L1^p L2^p/Q^p) integral g Q^p for Q = L1 L2. + /// + /// + /// L1^p L2^p/Q^p is constant wherever it is continuous -- it is 1 where both linears + /// are positive and a fixed root of unity on each interval between their roots -- so its + /// derivative is zero and the product differentiates back. Rubi's 1.1.1.3 + /// (e x)^(3/2)/((1 - x)^(1/4) (1 + x)^(1/4)) was declined, and + /// (2 + 3 x)^4/((1 - 2 x)^(5/2) (3 + 5 x)^(5/2)) took forty-five seconds; over + /// (1 - x^2)^(1/4) and (3 - x - 10 x^2)^(5/2) they take half a second. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveTwoLinearsToOnePowerAsOneQuadratic(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 Entity Over(Entity a, Entity b) => a / b; + var (numerator, denominator) = Functions.SingleQuotient.Of(expr); + var factors = new List<(Entity Base, Number.Rational Power, bool Below, Entity Factor)>(); + 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, Number.Rational power) && power is not Number.Integer + && TreeAnalyzer.TryGetPolyLinear(@base, x, out var slope, out _) && !VanishesIdentically(slope)) + factors.Add((@base, power, below, factor)); + else + rest = below ? Over(rest, factor) : Times(rest, factor); + } + Number.Rational Signed((Entity Base, Number.Rational Power, bool Below, Entity Factor) f) => f.Below ? (Number.Rational)(-f.Power) : f.Power; + for (var i = 0; i < factors.Count; i++) + for (var j = i + 1; j < factors.Count; j++) + { + var (first, second) = (factors[i], factors[j]); + var power = Signed(first); + if (power != Signed(second) + // Square roots of two linears belong to the rules for Rubi's 1.1.1.6 and + // to the combined radicals, which write the sign as sgn; and with symbols in + // the linears the rules for each of them answer, in a tenth of the time. + || power == Number.Rational.Create(1, 2) || power == Number.Rational.Create(-1, 2) + || (first.Base * second.Base).Vars.Any(symbol => symbol != x) + || !TreeAnalyzer.TryGetPolyLinear(first.Base, x, out var s1, out var c1) + || !TreeAnalyzer.TryGetPolyLinear(second.Base, x, out var s2, out var c2) + // Constant multiples of one linear are one linear, not a quadratic. + || VanishesIdentically((s1 * c2 - s2 * c1).Expand())) + continue; + var quadratic = (first.Base * second.Base).Expand().InnerSimplified; + Entity others = rest; + for (var k = 0; k < factors.Count; k++) + if (k != i && k != j) + others = factors[k].Below ? Over(others, factors[k].Factor) : Times(others, factors[k].Factor); + // Spelled as a quotient where the power is negative, which is how the rules for a + // power of a quadratic read it: as `g Q^(-5/2)` it was as slow as the split form. + var asked = power.IsNegative ? Over(others, MathS.Pow(quadratic, (Number.Rational)(-power))) : Times(others, MathS.Pow(quadratic, power)); + if (Integration.ComputeAsAQuestionOfItsOwn(asked, x, integrateByParts) is not { } joined) + return null; + // What is checked is the joined integral against the joined integrand: the constant + // is constant by the argument above, and differentiated it is a sum the sampled + // check does not reduce to zero. + if (!Functions.PartialFractions.DerivativeHoldsAtSampledPoints(joined, asked, x)) + return null; + var constant = MathS.Pow(first.Base, power) * MathS.Pow(second.Base, power) / MathS.Pow(quadratic, power); + return constant * joined; + } + return null; + } + /// /// 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 0b3cbfba1..480b29622 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -665,6 +665,10 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // over each linear once with the constant that takes in front. First, since it answers // the question asked only, and the polynomial term below writes `1/u` as `u^(-1)` and // asks that a level down: `1/(e^atanh(a x) (c - c/(a x))^(3/2))` reached it nowhere. + // Two linears to one power that is not whole, as one quadratic to that power, before each + // linear is written once: `(2 + 3x)^4/((1 - 2x)^(5/2) (3 + 5x)^(5/2))` took forty-five + // seconds there, and over `(3 - x - 10x^2)^(5/2)` it takes half of one. + if ((answer = IndefiniteIntegralSolver.SolveTwoLinearsToOnePowerAsOneQuadratic(expr, x, integrateByParts)) is { }) return answer; if ((answer = IndefiniteIntegralSolver.SolveByWritingEachLinearOnce(expr, x, integrateByParts)) is { }) return answer; // A rational function with numbers for coefficients over a denominator past the twelfth // degree, by Hermite's reduction before anything searches: nothing below answered one. diff --git a/Sources/Tests/UnitTests/Calculus/TwoLinearsToOnePowerIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/TwoLinearsToOnePowerIntegralTest.cs new file mode 100644 index 000000000..128ddf4bd --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/TwoLinearsToOnePowerIntegralTest.cs @@ -0,0 +1,69 @@ +// +// 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 +{ + /// + /// Two linears to one power that is not whole, integrated as one quadratic to that power with + /// L1^p L2^p/(L1 L2)^p in front, which is constant on each interval it is continuous on. + /// Rubi's 1.1.1.3, which declined the quarter powers and took forty-five seconds over the + /// powers of 5/2. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class TwoLinearsToOnePowerIntegralTest + { + private static void DifferentiatesBack(string integrand, Entity[] points, 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 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-8, + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } + + private static readonly Entity[] TheLine = { "-23/10", "-7/10", "-3/10", "2/5", "9/10", "5/2" }; + + // Where 1 - 2x and 3 + 5x are positive: outside, the integrand is complex and the answer's + // arcsin is past its real interval, which the evaluator reads as NaN at its default + // precision; the answer agrees with the integrand there to 2000 digits. + private static readonly Entity[] BetweenTheRoots = { "-11/20", "-3/10", "0", "1/5", "9/20" }; + + [Theory] + [InlineData("(e*x)^(3/2)/((1-x)^(1/4)*(1+x)^(1/4))")] + [InlineData("1/((1-x)^(1/4)*(e*x)^(1/2)*(1+x)^(1/4))")] + public void TwoQuarterPowers(string integrand) + { + DifferentiatesBack(integrand, TheLine, ("e", 1.5)); + DifferentiatesBack(integrand, TheLine, ("e", -0.75)); + } + + [Theory] + [InlineData("(2+3*x)^4/((1-2*x)^(5/2)*(3+5*x)^(5/2))")] + [InlineData("1/((1-2*x)^(5/2)*(2+3*x)^2*(3+5*x)^(5/2))")] + public void TwoPowersOfFiveHalves(string integrand) => DifferentiatesBack(integrand, BetweenTheRoots); + } +}