diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index c75e5a311..d021fe3cf 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -309,6 +309,20 @@ power up each step, and what reaches the first power is integrated once over the | `"1/(a + b*x^2 + c*x^4)^3".ToEntity().Integrate("x")` | `integral(...)` | the same | | `"x/(a + b*x^3 + c*x^6)^2".ToEntity().Integrate("x")` | `integral(...)` | a polynomial over `a + b x^3 + c x^6`, and the integral of a polynomial over it | +### A polynomial over a power of one linear with a symbol in it is written in powers of the linear + +**Answers where there were none.** `t^9/(a + b t)^8` is a polynomial and eight powers of `1/(a + b t)`, +and was declined by 2.5.0. On the unreleased master it was answered after forty-six seconds: divided +out and decomposed with the symbols in it, it went round the substitution search a dozen levels deep. +With nothing else below the bar and a symbol in the linear, the polynomial is written in powers of +the linear at its root now, and each piece is the table's +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"t^9/(a + b*t)^8".ToEntity().Integrate("t")` | `integral(...)` | powers of `a/b + t` and a logarithm, in 0.03 s | +| `"x^4/(a + b*sqrt(x))^8".ToEntity().Integrate("x")` | `integral(...)` | the same in `sqrt(x)`, in 0.5 s; a minute on the unreleased master | + ### A polynomial over a power of a quadratic with a sum of symbols in it is reduced **Answers where there were none.** The reduction of `N(x)/Q(x)^n` divides `N` by the quadratic a diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 2ac4d83e4..131ed2a1a 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -644,6 +644,11 @@ Entity Back(Entity mapped) return SolveByPartialFractions(reducedTop / reducedBottom, x, integrateByParts) ?? Integration.ComputeIndefiniteIntegral(reducedTop / reducedBottom, x, integrateByParts); + // A polynomial over a power of one linear with a symbol in it, in powers of the linear at + // its root, before the division: see SolveByReducingThePolynomialOverALinearFactor. + if (SolveByReducingThePolynomialOverALinearFactor(expr, x, integrateByParts, alone: true) is { } overThePowersOfTheLinear) + return overThePowersOfTheLinear; + // The helper answers null for a fraction that is already proper, so this cannot // fire on one and recurse into the problem it started from. The check on the // quotient is the second half of that guarantee: a division that came back with @@ -2207,6 +2212,18 @@ over is Entity.Powf(var @base, var power) ? /// https://github.com/asc-community/AngouriMath/issues/718 /// internal static Entity? SolveByReducingThePolynomialOverALinearFactor(Entity expr, Entity.Variable x, bool integrateByParts) + => SolveByReducingThePolynomialOverALinearFactor(expr, x, integrateByParts, alone: false); + + /// + /// , + /// or with a polynomial over a power of one linear and nothing else, + /// that linear with a symbol in it: t^9/(a + b t)^8 is a polynomial and eight powers of + /// 1/(a + b t), each the table's. asks it so before + /// it divides: divided out and decomposed with the symbols in it, that one went round the + /// substitution search a dozen levels deep and took forty-six seconds, and with numbers in + /// place of a and b it took sixty milliseconds. + /// + private static Entity? SolveByReducingThePolynomialOverALinearFactor(Entity expr, Entity.Variable x, bool integrateByParts, bool alone) { if (!TryReadAsQuotient(expr, out var numerator, out var denominator)) return null; @@ -2241,11 +2258,13 @@ over is Entity.Powf(var @base, var power) ? somethingElse = true; rest *= factor; } - if (linear is null || !somethingElse) + if (linear is null || (alone ? somethingElse || rest.ContainsNode(x) || multiplicity < 2 : !somethingElse)) return null; if (!TreeAnalyzer.TryGetPolynomial(linear, x, out var line)) return null; + if (alone && !line.Values.Any(coefficient => coefficient.Vars.Any())) + return null; var beta = line[EInteger.One]; var alpha = line.TryGetValue(EInteger.Zero, out var constantTerm) ? constantTerm : Number.Integer.Zero; var root = Functions.PartialFractions.Bare((-alpha / beta).InnerSimplified); diff --git a/Sources/Tests/UnitTests/Calculus/PolynomialOverAPowerOfASymbolicLinearTest.cs b/Sources/Tests/UnitTests/Calculus/PolynomialOverAPowerOfASymbolicLinearTest.cs new file mode 100644 index 000000000..0f11115b2 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/PolynomialOverAPowerOfASymbolicLinearTest.cs @@ -0,0 +1,55 @@ +// +// 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 polynomial over a power of one linear with a symbol in it, written in powers of the + /// linear at its root: t^9/(a + b t)^8 is a polynomial and eight powers of + /// 1/(a + b t). Divided out and decomposed with the symbols in it, that one took + /// forty-six seconds, and x^4/(a + b sqrt(x))^8, which is it under x = t^2, + /// a minute; the sizes here are the ones that were slow, so that a regression shows as a + /// slow suite rather than as a clock in a test. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class PolynomialOverAPowerOfASymbolicLinearTest + { + [Theory] + [InlineData("x^9/(a + b*x)^8")] + [InlineData("x^6/(a + b*x)^5")] + [InlineData("x^9/(a + x)^8")] + [InlineData("(c + d*x)^3/(a + b*x)^5")] + [InlineData("x^4/(a + b*sqrt(x))^8")] + public void InPowersOfTheLinear(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).Substitute("d", 1.7); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -1.7, -0.9, 0.3, 0.8, 1.6, 2.9 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + if (want.IsNaN || Math.Abs((double)want.ImaginaryPart) > 1e-12) + continue; + compared++; + var got = derivative.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}"); + } + Assert.True(compared >= 3, $"only {compared} points could be compared for {integrand}"); + } + } +}