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}");
+ }
+ }
+}