diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index f0b64c24b..cc36da245 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -396,6 +396,19 @@ 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 linear and a quadratic to one power are integrated as one binomial + +**Answers where there were none.** `1/((1 + x)^(1/3) (1 - x + x^2)^(1/3))` was declined. A linear and a quadratic to +one power that is not whole, whose product is `A + B x^3`, are now integrated as `(L^p Q^p/(A + B x^3)^p)` times the +integral over `(A + B x^3)^p`. The 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)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/((1+x)^(1/3)*(1-x+x^2)^(1/3))".ToEntity().Integrate("x")` | `integral(...)` | 263 characters | +| `"1/((1-x)^(1/3)*(1+x+x^2)^(1/3))".ToEntity().Integrate("x")` | `integral(...)` | 269 characters | +| `"1/((b*k-c*k*x)^(1/3)*(b^2+b*c*x+c^2*x^2)^(1/3))".ToEntity().Integrate("x")` | `integral(...)` | 881 characters | + ### A sine or a cosine over a symbolic quadratic or binomial is split over its roots with the coefficients named **Answers where there were none.** A quadratic or a binomial with a compound coefficient below a sine or a cosine is diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 30b24cbbb..5a0a8ccab 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -19919,6 +19919,75 @@ Piecewise piecewise when piecewise.Cases.Any() => piecewise.Cases.First().Expres return Functions.PartialFractions.DerivativeHoldsAtSampledPoints(answer, expr, x) ? answer : null; } + /// + /// A linear and a quadratic to one power that is not whole, as one binomial: + /// integral g L^p Q^p = (L^p Q^p/P^p) integral g P^p where P = L Q is + /// A + B x^3, as (1 + x) (1 - x + x^2) is 1 + x^3. + /// + /// + /// L^p Q^p/P^p is constant wherever it is continuous, for the reason + /// gives, and the rules for a power + /// of a binomial answer the rest. Rubi's 1.2.1.2 1/((1 + x)^(1/3) (1 - x + x^2)^(1/3)) + /// and 1/((b e - c e x)^(1/3) (b^2 + b c x + c^2 x^2)^(1/3)) were declined. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveALinearAndAQuadraticToOnePowerAsOneBinomial(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; + int? Degree(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())).Select(term => term.Key).ToList(); + return present.Count == 0 || present.Max() is not { } top || !top.CanFitInInt32() ? null : top.ToInt32Unchecked(); + } + var (numerator, denominator) = Functions.SingleQuotient.Of(expr); + var factors = new List<(Entity Base, Number.Rational Power, bool Below, Entity Factor, int Degree)>(); + 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 + && Degree(@base) is (1 or 2) and var degree) + factors.Add((@base, power, below, factor, degree)); + else + rest = below ? Over(rest, factor) : Times(rest, factor); + } + Number.Rational Signed((Entity Base, Number.Rational Power, bool Below, Entity Factor, int Degree) f) => f.Below ? (Number.Rational)(-f.Power) : f.Power; + for (var i = 0; i < factors.Count; i++) + for (var j = 0; j < factors.Count; j++) + { + var (first, second) = (factors[i], factors[j]); + var power = Signed(first); + if (first.Degree != 1 || second.Degree != 2 || power != Signed(second) + || !TreeAnalyzer.TryGetPolynomial(first.Base * second.Base, x, out var product)) + continue; + var terms = product.Where(term => !VanishesIdentically(term.Value.Expand())).ToDictionary(term => term.Key, term => term.Value); + if (terms.Count != 2 || !terms.ContainsKey(EInteger.Zero) || !terms.ContainsKey(EInteger.FromInt32(3))) + continue; + var binomial = terms[EInteger.Zero].Expand().InnerSimplified + + terms[EInteger.FromInt32(3)].Expand().InnerSimplified * MathS.Pow(x, Number.Integer.Create(3)); + 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, as the rules for a power of a + // binomial read it. + var asked = power.IsNegative ? Over(others, MathS.Pow(binomial, (Number.Rational)(-power))) : Times(others, MathS.Pow(binomial, power)); + if (Integration.ComputeAsAQuestionOfItsOwn(asked, x, integrateByParts) is not { } joined) + return null; + if (!Functions.PartialFractions.DerivativeHoldsAtSampledPoints(joined, asked, x)) + return null; + var constant = MathS.Pow(first.Base, power) * MathS.Pow(second.Base, power) / MathS.Pow(binomial, 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 055b4fba4..606c7cfd4 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -669,6 +669,9 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // 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; + // A linear and a quadratic to one power as one binomial, `(1 + x) (1 - x + x^2)` as + // `1 + x^3`, which the rules for a power of a binomial answer. + if ((answer = IndefiniteIntegralSolver.SolveALinearAndAQuadraticToOnePowerAsOneBinomial(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/ALinearAndAQuadraticToOnePowerIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ALinearAndAQuadraticToOnePowerIntegralTest.cs new file mode 100644 index 000000000..20597337e --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ALinearAndAQuadraticToOnePowerIntegralTest.cs @@ -0,0 +1,61 @@ +// +// 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 linear and a quadratic to one power whose product is a binomial in x^3, + /// integrated over the binomial: (1 + x) (1 - x + x^2) is 1 + x^3. Rubi's + /// 1.2.1.2 rows, sampled on both sides of the linear's root, where the cube roots are complex. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class ALinearAndAQuadraticToOnePowerIntegralTest + { + 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/((1+x)^(1/3)*(1-x+x^2)^(1/3))")] + [InlineData("1/((1-x)^(1/3)*(1+x+x^2)^(1/3))")] + public void WithNumbersIsAnsweredAndDifferentiatesBack(string integrand) + => DifferentiatesBack(integrand); + + [Fact] + public void WithSymbolsIsAnsweredAndDifferentiatesBack() + { + const string integrand = "1/((b*k-c*k*x)^(1/3)*(b^2+b*c*x+c^2*x^2)^(1/3))"; + DifferentiatesBack(integrand, ("b", 2), ("c", 3), ("k", 0.7)); + DifferentiatesBack(integrand, ("b", -2), ("c", 3), ("k", -1.5)); + } + } +}