diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 914d0be9a..30ac30458 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -232,6 +232,25 @@ Rubi's `x^m (a + b x^n)^p` and `(a + b x^n)^p (c + d x^n)^q` | `"1/(2+3/x^2)^3".Integrate("x")`, `1/(a + b/x^3)` | left unevaluated | an antiderivative | | `"(a+b*x^n)*(c+d*x^n)^3".Integrate("x")` | left unevaluated | written out, eight powers of `x` integrated: `a c^3 x + ... + b d^3 x^(4 n + 1)/(4 n + 1)` | +### A whole power of a quotient with a symbol in it is integrated as the quotient of the powers + +**Answers where there were none, and a slowdown since 2.5.0 undone.** `(c/(a + c x^2))^2` was +declined, after twenty-six seconds, while `c^2/(a + c x^2)^2`, the same function, was answered at +once: the table reads the quotient of the powers and nothing read the power of the quotient. It is +written so now wherever the quotient is of two polynomials in `x` with a symbol in them and what +it leaves above the bar is a monomial, the table's `x^m/(a + b x^n)^p`; that is exact for a whole +power. That is +also how the substitution `u = x^2` writes `x/(a + c x^4)^2` since 2.5.0, having simplified +`1/(a/c + u^2)^2`, and that integrand took twenty seconds where 2.5.0 took a tenth of one +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"(c/(a + c*x^2))^2".ToEntity().Integrate("x")` | `integral(...)`, after a minute | a quotient and an arctangent or a logarithm, by the sign of `a c` | +| `"(1/(a + c*x^2))^2".ToEntity().Integrate("x")` | `integral(...)` | the same | +| `"(x/(a + c*x^2))^2".ToEntity().Integrate("x")` | `integral(...)` | the same | +| `"x/(a + c*x^4)^2".ToEntity().Integrate("x")` | the answer, in 0.1 s; 22 s on the unreleased master | the answer, in 0.06 s | + ### A polynomial with symbols in it that is a binomial or an even quartic in a shifted variable is integrated in it **Answers where there were none.** `1/(c^2 x^3 + 3 b c x^2 + 3 b^2 x + 3 a b)` was left unevaluated, diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 17ade5dd3..783c208e8 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -17021,6 +17021,44 @@ node is Powf(Mulf(var l, var r) product, Number.Integer power) && product.Contai return distributed == expr ? null : Integration.ComputeAsTheSameQuestion(distributed, x, integrateByParts); } + /// + /// A whole power of a quotient with a symbol in it, written as the quotient of the powers: + /// (c/(a + c u^2))^2 is c^2/(a + c u^2)^2, exact for a whole power. That is how + /// the substitution u = x^2 writes x/(a + c x^4)^2, having simplified + /// 1/(a/c + u^2)^2, and the table reads the quotient of the powers and not the power + /// of the quotient: c^2/(a + c x^2)^2 was answered in a few milliseconds where + /// (c/(a + c x^2))^2 went to parts and was declined after twenty-six seconds, and + /// x/(a + c x^4)^2 was answered after forty, by another route. With numbers only it + /// is answered as it is already. Only a quotient of two polynomials in : + /// the hyperbolic functions arrive as quotients of exponentials, and the imaginary tangent's + /// sum as A e^(i z)/cos(z), and written apart those went to slower routes -- + /// cosh(c + d x)/(a + b tanh(c + d x)^2) from 17 ms to past five seconds. And only with + /// a monomial left above the bar, the table's x^m/(a + b x^n)^p: a sum there is + /// multiplied out by the rational rules, and the folded tanh(a + 2 ln(x))^2, + /// ((e^(2a) x^4 - 1)/(e^(2a) x^4 + 1))^2, went from five seconds to forty-five that way. + /// Asked as the same question. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveByDistributingWholePowersOfQuotients(Entity expr, Entity.Variable x, bool integrateByParts) + { + var distributed = expr.Replace(node => + node is Powf(Divf(var above, var below) quotient, Number.Integer power) && quotient.ContainsNode(x) + && quotient.Vars.Any(symbol => symbol != x) + && IsAPolynomialIn(above, x) && IsAPolynomialIn(below, x) + && IsAMonomialIn(power.EInteger.Sign > 0 ? above : below, x) + ? power.EInteger.Sign > 0 + ? MathS.Pow(above, power) / MathS.Pow(below, power) + : MathS.Pow(below, Number.Integer.Create(power.EInteger.Negate())) / MathS.Pow(above, Number.Integer.Create(power.EInteger.Negate())) + : node); + return distributed == expr ? null : Integration.ComputeAsTheSameQuestion(distributed, x, integrateByParts); + + static bool IsAPolynomialIn(Entity expr, Entity.Variable x) + => TreeAnalyzer.TryGetPolynomial(expr, x, out var terms) + && terms.Keys.All(power => power.Sign >= 0) && terms.Values.All(coefficient => !coefficient.ContainsNode(x)); + static bool IsAMonomialIn(Entity expr, Entity.Variable x) + => TreeAnalyzer.TryGetPolynomial(expr, x, out var terms) && terms.Count == 1; + } + /// /// A polynomial in x times a rational function of exponentials of x, /// by parts against the whole rational function: x tanh(x)^2 is diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 087ac67fb..16e7e4282 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -684,6 +684,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => // A whole power of a product of a constant and the variable, as the product of // the powers, which is how the inverse hyperbolic secant and cosecant arrive. if ((answer = IndefiniteIntegralSolver.SolveByDistributingWholePowersOfProducts(expr, x, integrateByParts)) is { }) return answer; + // And of quotients, which a substitution leaves where it has simplified a sum over + // the bar: `(c/(a + c u^2))^2` is read by nothing that reads `c^2/(a + c u^2)^2`. + if ((answer = IndefiniteIntegralSolver.SolveByDistributingWholePowersOfQuotients(expr, x, integrateByParts)) is { }) return answer; // And a fractional or symbolic power of a monomial: `(c x^n)^b` is `c^b x^(n b)` // for a positive `c`, on the `x > 0` where a symbolic `n` leaves the integrand real. if ((answer = IndefiniteIntegralSolver.SolveByDistributingAPowerOfAMonomial(expr, x, integrateByParts)) is { }) return answer; diff --git a/Sources/Tests/UnitTests/Calculus/WholePowerOfAQuotientIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/WholePowerOfAQuotientIntegralTest.cs new file mode 100644 index 000000000..cd8fa691e --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/WholePowerOfAQuotientIntegralTest.cs @@ -0,0 +1,53 @@ +// +// 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 whole power of a quotient with a symbol in it is the quotient of the powers: + /// (c/(a + c x^2))^2 is c^2/(a + c x^2)^2, which the table answers, and was + /// declined. It is how the substitution u = x^2 writes x/(a + c x^4)^2. + /// #718 + /// + /// + /// Checked by differentiating back with a = 2.3, b = 0.7, c = 1.3, on + /// both sides of zero. + /// + [Trait("Area", "Calculus")] + public sealed class WholePowerOfAQuotientIntegralTest + { + [Theory] + [InlineData("(c/(a + c*x^2))^2")] + [InlineData("(1/(a + c*x^2))^2")] + [InlineData("(x/(a + c*x^2))^2")] + [InlineData("(c/(a + c*x^2))^3")] + [InlineData("((a + c*x^2)/x)^(-2)")] + [InlineData("x/(a + c*x^4)^2")] + [InlineData("x^2/(a + b*x^6)^2")] + public void IsTheQuotientOfThePowers(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Assert.DoesNotContain("NaN", integral.Stringize()); + Entity Pinned(Entity e) => e.Substitute("a", 2.3).Substitute("b", 0.7).Substitute("c", 1.3); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in new[] { -1.4, -0.6, 0.5, 1.2 }) + { + var got = derivative.Substitute("x", at).EvalNumerical(); + var want = original.Substitute("x", at).EvalNumerical(); + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart)), + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } + } +}