diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index c975c1c59..fc26529d2 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -193,6 +193,21 @@ of a linear over a linear, which is answered in a second
| `"1/(sqrt(a + i*a*tan(x))*(c + d*tan(x))^(3/2))".ToEntity().Integrate("x")` | `integral(...)`; past thirty seconds on the unreleased master | 2,217 characters |
| `"1/((a + i*a*tan(x))^(3/2)*(c + d*tan(x))^(5/2))".ToEntity().Integrate("x")` | `integral(...)`; past thirty seconds on the unreleased master | 3,333 characters |
+### A symbolic quadratic with a square discriminant is split into its linears before the division
+
+**Answers where there were none.** `(d + e x)^8/(a d e + (c d^2 + a e^2) x + c d e x^2)^2` ran past a minute, with
+eleven more of Rubi's 1.2.1.2: a power of a linear over a power of a quadratic that is that linear times another.
+The quadratic's discriminant is the square of a polynomial in the symbols, so it is written as its two linears,
+also where it has been made monic and its coefficients stand over the leading one; the power the numerator shares
+then cancels before the division of the improper fraction
+([#718](https://github.com/asc-community/AngouriMath/issues/718)).
+
+| Input | Was (2.5.0) | Now |
+|---|---|---|
+| `"(d + e*x)^8/(a*d*e + (c*d^2 + a*e^2)*x + c*d*e*x^2)^2".ToEntity().Integrate("x")` | past a minute | 5,126 characters |
+| `"(a + b*x)^6/(a*c + (b*c + a*d)*x + b*d*x^2)^2".ToEntity().Integrate("x")` | past a minute | 3,592 characters |
+| `"csc(e + f*x)^3/(a + b*tan(e + f*x)^2)^2".ToEntity().Integrate("x")` | `integral(...)` | 7,992 characters |
+
### One linear twice is not two linear powers
**Answers that had no value.** `(a c + b c x)^(-3 - 2 p) (f + g x) (a^2 + 2 a b x + b^2 x^2)^p` was answered
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
index 009dc96c6..76c5c9b42 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
@@ -724,6 +724,23 @@ Entity Back(Entity mapped)
if (SolveByReducingThePolynomialOverALinearFactor(expr, x, integrateByParts, alone: true) is { } overThePowersOfTheLinear)
return overThePowersOfTheLinear;
+ // A symbolic quadratic whose discriminant is a square in the symbols, written as its two
+ // linears before the division: the numerator may share one of them, and over the
+ // quadratic as written the division of `(d + e x)^8` ran past a minute.
+ // The numerator's sums are written as the split writes its linears, so that a power the
+ // two sides share is gathered and cancelled before anything else reads the quotient.
+ // Only where the numerator shares one of the linears, which is what the split is for:
+ // splitting `1 - c^2 x^2` that parts left beside an inverse hyperbolic cotangent, with
+ // nothing to cancel, sent 7.4.1's `(a + b arccoth(c x)) (d + e ln(1 - c^2 x^2))` from
+ // thirteen seconds past two minutes.
+ if (WithSymbolicQuadraticsInAPowerSplit(denominator, x, 1) is { } overLinears
+ && WithSumsAsPolynomialsInTheSymbols(numerator, x) is var writtenAlike
+ && SharesAFactor(writtenAlike, overLinears, x)
+ && Patterns.GatherPowersOfOneBase(writtenAlike / overLinears) is var overTheSplit
+ && (SolveByPartialFractions(overTheSplit, x, integrateByParts)
+ ?? Integration.ComputeIndefiniteIntegral(overTheSplit, x, integrateByParts)) is { } overTheLinearsOfAQuadratic)
+ return overTheLinearsOfAQuadratic;
+
// 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
@@ -750,6 +767,12 @@ is var (multiple, leftover)
&& (leftover is Divf(var leftoverTop, _) ? leftoverTop : leftover).InnerSimplified.Evaled is Number.Complex { IsZero: true })
return (multiple * MathS.Ln(denominator)).InnerSimplified;
+ // A symbolic quartic in x^2 that is two quadratics in it, written as them first.
+ if (WithSymbolicBiquadraticsSplit(denominator, x) is { } split
+ && (SolveByPartialFractions(numerator / split, x, integrateByParts)
+ ?? Integration.ComputeIndefiniteIntegral(numerator / split, x, integrateByParts)) is { } overTheQuadratics)
+ return overTheQuadratics;
+
// A written sum with a symbol among its coefficients that they all share is that
// symbol times a polynomial over the rationals, and is written so first: `(a u + a)`
// beside `1 - u^2` shares a root with it, which the symbolic split declines, and a
@@ -761,12 +784,6 @@ is var (multiple, leftover)
// And the powers of x it takes out joined to the ones beside them: `x (a x + b x^3 + c x^5)^2`
// is `x^3 (a + b x^2 + c x^4)^2`, and written `x x^2 (a + b x^2 + c x^4)^2` the splits
// below read `x` and `x^2` as two factors and the search ran past a minute.
- // A symbolic quartic in x^2 that is two quadratics in it, written as them first.
- if (WithSymbolicBiquadraticsSplit(denominator, x) is { } split
- && (SolveByPartialFractions(numerator / split, x, integrateByParts)
- ?? Integration.ComputeIndefiniteIntegral(numerator / split, x, integrateByParts)) is { } overTheQuadratics)
- return overTheQuadratics;
-
if (WithTheContentOutOfEachSumFactor(denominator, x) is { } primitive
&& Patterns.GatherPowersOfOneBase(numerator / primitive) is var overThePrimitives
&& (SolveByPartialFractions(overThePrimitives, x, integrateByParts)
@@ -15041,6 +15058,54 @@ private static bool IsASumOfMonomials(Entity expr, Entity.Variable x)
return true;
}
+ ///
+ /// Whether vanishes at the root of a linear in
+ /// among the factors of , or the bases of their powers: whether the
+ /// two share that linear, up to a constant.
+ ///
+ private static bool SharesAFactor(Entity above, Entity below, Entity.Variable x)
+ {
+ // A polynomial numerator only: a logarithm in it is no value at a root, not zero there.
+ if (!TreeAnalyzer.TryGetPolynomial(above, x, out _))
+ return false;
+ foreach (var factor in Mulf.LinearChildren(below))
+ {
+ var @base = factor is Powf(var b, Number.Integer) ? b : factor;
+ if (@base is not (Sumf or Minusf) || !TreeAnalyzer.TryGetPolyLinear(@base, x, out var slope, out var intercept)
+ || slope.ContainsNode(x) || intercept.ContainsNode(x) || TreeAnalyzer.IsZero(slope))
+ continue;
+ if (Functions.PartialFractions.IsZeroAsAValue(above.Substitute(x, -intercept / slope)))
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// with each sum among its factors, or the base of a power of one,
+ /// written as writes it: the spelling
+ /// gives its factors, so that equal
+ /// factors on the two sides of a bar are written alike.
+ ///
+ private static Entity WithSumsAsPolynomialsInTheSymbols(Entity expr, Entity.Variable x)
+ {
+ Entity product = Number.Integer.One;
+ foreach (var factor in Mulf.LinearChildren(expr))
+ {
+ var (@base, power) = factor is Powf(var b, Number.Integer p) ? (b, (Entity)p) : (factor, (Entity)Number.Integer.One);
+ if (@base is (Sumf or Minusf) && @base.ContainsNode(x))
+ {
+ var variables = @base.Vars.OrderBy(v => v.Name, System.StringComparer.Ordinal).ToList();
+ var indices = new Dictionary();
+ for (var i = 0; i < variables.Count; i++)
+ indices[variables[i]] = i;
+ if (variables.Count <= MultivariatePolynomial.MaxVariables && MultivariatePolynomial.TryParse(@base, indices) is { } polynomial)
+ @base = polynomial.ToEntity(variables);
+ }
+ product *= power == Number.Integer.One ? @base : MathS.Pow(@base, power);
+ }
+ return product;
+ }
+
///
/// with each written factor A x^4 + B x^2 + C with symbols
/// in it whose discriminant B^2 - 4AC is the square of a polynomial S in them
@@ -15055,16 +15120,40 @@ private static bool IsASumOfMonomials(Entity expr, Entity.Variable x)
/// https://github.com/asc-community/AngouriMath/issues/718
///
private static Entity? WithSymbolicBiquadraticsSplit(Entity denominator, Entity.Variable x)
+ => WithSymbolicQuadraticsInAPowerSplit(denominator, x, 2);
+
+ ///
+ /// with each written factor A x^(2k) + B x^k + C with
+ /// symbols in it whose discriminant is the square of a polynomial S in them written as
+ /// (2A x^k + B - S)(2A x^k + B + S)/(4A); where there is none.
+ /// For k = 1, a quadratic as its two linears.
+ ///
+ ///
+ /// Rubi's 1.2.1.2 has `(d + e x)^8/(a d e + (c d^2 + a e^2) x + c d e x^2)^2` and its kind, a
+ /// power of a linear over a power of a quadratic that is that linear times another. As written
+ /// the division of the improper fraction ran past a minute; over `(d + e x)(a e + c d x)` the
+ /// shared factor cancels and the rest is answered in a tenth of a second.
+ /// https://github.com/asc-community/AngouriMath/issues/718
+ ///
+ private static Entity? WithSymbolicQuadraticsInAPowerSplit(Entity denominator, Entity.Variable x, int step)
{
+ var (low, middleDegree, high) = (EInteger.Zero, EInteger.FromInt32(step), EInteger.FromInt32(2 * step));
var changed = false;
Entity product = Number.Integer.One;
foreach (var factor in Mulf.LinearChildren(denominator))
{
var (@base, power) = factor is Powf(var b, Number.Integer { EInteger.Sign: > 0 } p) ? (b, p) : (factor, Number.Integer.One);
+ // A factor made monic has its coefficients over the leading one's symbols: split what is
+ // over the bar, and keep the bar.
+ Entity below = Number.Integer.One;
+ if (@base is (Sumf or Minusf) && @base.Nodes.Any(node => node is Divf(_, var under) && under.Vars.Any())
+ && Functions.SingleQuotient.Of(Functions.SingleQuotient.Combine(@base)) is var (overTheBar, underTheBar)
+ && !underTheBar.ContainsNode(x) && underTheBar != Number.Integer.One)
+ (@base, below) = (overTheBar, underTheBar);
if (@base is not (Sumf or Minusf) || !@base.ContainsNode(x) || !@base.Vars.Any(v => v != x)
|| !TreeAnalyzer.TryGetPolynomial(@base, x, out var read)
- || !read.Keys.All(degree => degree.Equals(EInteger.Zero) || degree.Equals(EInteger.FromInt32(2)) || degree.Equals(EInteger.FromInt32(4)))
- || !read.ContainsKey(EInteger.FromInt32(4)) || !read.ContainsKey(EInteger.Zero))
+ || !read.Keys.All(degree => degree.Equals(low) || degree.Equals(middleDegree) || degree.Equals(high))
+ || !read.ContainsKey(high) || !read.ContainsKey(low))
{
product *= factor;
continue;
@@ -15080,16 +15169,16 @@ private static bool IsASumOfMonomials(Entity expr, Entity.Variable x)
indices[variables[i]] = i;
var at = indices[x];
if (MultivariatePolynomial.TryParse(@base, indices) is not { } polynomial
- || !polynomial.CoefficientsIn(at).TryGetValue(4, out var a)
+ || !polynomial.CoefficientsIn(at).TryGetValue(2 * step, out var a)
|| !polynomial.CoefficientsIn(at).TryGetValue(0, out var c))
{
product *= factor;
continue;
}
- var b2 = polynomial.CoefficientsIn(at).TryGetValue(2, out var middle) ? middle : MultivariatePolynomial.Zero(variables.Count);
+ var b2 = polynomial.CoefficientsIn(at).TryGetValue(step, out var middle) ? middle : MultivariatePolynomial.Zero(variables.Count);
if (b2.Multiply(b2) is not { } bSquared || a.Multiply(c) is not { } ac
|| bSquared.Subtract(ac.ScaleBy(ERational.FromInt32(4))).TrySquareRoot() is not { IsZero: false } root
- || a.ScaleBy(ERational.FromInt32(2)).ShiftedBy(at, 2) is not { } twiceA)
+ || a.ScaleBy(ERational.FromInt32(2)).ShiftedBy(at, step) is not { } twiceA)
{
product *= factor;
continue;
@@ -15098,8 +15187,8 @@ private static bool IsASumOfMonomials(Entity expr, Entity.Variable x)
var second = twiceA.Add(b2).Add(root).ToEntity(variables);
var scale = a.ScaleBy(ERational.FromInt32(4)).ToEntity(variables);
product *= power == Number.Integer.One
- ? first * second / scale
- : MathS.Pow(first, power) * MathS.Pow(second, power) / MathS.Pow(scale, power);
+ ? first * second / (scale * below)
+ : MathS.Pow(first, power) * MathS.Pow(second, power) / MathS.Pow(scale * below, power);
changed = true;
}
return changed ? product : null;
diff --git a/Sources/Tests/UnitTests/Calculus/ASymbolicQuadraticSplitIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ASymbolicQuadraticSplitIntegralTest.cs
new file mode 100644
index 000000000..a4ee68bd4
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/ASymbolicQuadraticSplitIntegralTest.cs
@@ -0,0 +1,50 @@
+//
+// 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 symbolic quadratic whose discriminant is a square in the symbols, written as its two
+ /// linears before the partial fractions, so that a power of one the numerator shares cancels.
+ /// Rubi's 1.2.1.2.
+ /// #718
+ ///
+ [Trait("Area", "Calculus")]
+ public sealed class ASymbolicQuadraticSplitIntegralTest
+ {
+ [Theory]
+ [InlineData("(d + g*x)^8/(a*d*g + (c*d^2 + a*g^2)*x + c*d*g*x^2)^2")]
+ [InlineData("(a + b*x)^6/(a*c + (b*c + a*d)*x + b*d*x^2)^3")]
+ public void OverItsLinears(string integrand)
+ {
+ var integral = integrand.ToEntity().Integrate("x");
+ var text = integral.Stringize();
+ Assert.DoesNotContain("integral(", text);
+ Assert.True(text.Length < 40000, $"{text.Length} characters of answer for {integrand}");
+ Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.4).Substitute("c", 1.9).Substitute("d", 1.6).Substitute("f", 0.7).Substitute("g", 0.45);
+ var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x");
+ var original = Pinned(integrand.ToEntity());
+ var compared = 0;
+ foreach (var at in new[] { -2.1, -0.4, 0.3, 0.6, 1.6, 2.5 })
+ {
+ var want = original.Substitute("x", at).EvalNumerical();
+ var got = derivative.Substitute("x", at).EvalNumerical();
+ if (want.IsNaN)
+ continue;
+ compared++;
+ Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart)
+ < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)),
+ $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}");
+ }
+ Assert.True(compared >= 5, $"only {compared} points could be compared for {integrand}");
+ }
+ }
+}