From 39785e02599340c3e44f5ac27a983e8a4b2d85d3 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 15 Oct 2026 05:55:02 +0000 Subject: [PATCH 1/4] Lower the power of a symbolic quadratic in a rational function one at a time A rational function over a whole power of a symbolic quadratic, beside at most one linear, is taken apart at the linear's root -- through Q - Q(p) = (x - p)(c (x + p) + b), so that every piece stays a short polynomial over a power of L and of Q -- and the power of the quadratic is lowered by the recurrence for the integral of 1/Q^k, down to the first, which is written in closed form. The discriminant, Q(p) and p are named while it runs. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/IndefiniteIntegralSolver.cs | 179 ++++++++++++++++++ .../Integration/Integration.Definition.cs | 3 + ...rAPowerOfASymbolicQuadraticIntegralTest.cs | 60 ++++++ 3 files changed, 242 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/ARationalFunctionOverAPowerOfASymbolicQuadraticIntegralTest.cs diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 3a7a8a3b4..8db434c0d 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -19342,6 +19342,185 @@ Entity TakeOut(Entity node) return inTermsOfU.Substitute(u, Number.Integer.One / x); } + /// + /// A rational function over a whole power of a symbolic quadratic, beside at most one + /// linear to a whole power, with the power of the quadratic lowered one at a time rather + /// than the quadratic taken apart: the linear comes off first, as + /// N/(L^j Q^n) = N(p)/(Q(p)^n L^j) + M/(L^(j - 1) Q^n) at its root p, and + /// what is left over Q^k is S/Q^(k - 1) + (alpha Q' + beta)/Q^k, whose second + /// part is -alpha/((k - 1) Q^(k - 1)) and the recurrence + /// integral 1/Q^k = (2 c x + b)/((k - 1) D Q^(k - 1)) + 2 c (2 k - 3)/((k - 1) D) integral 1/Q^(k - 1) + /// with D = 4 a c - b^2, down to the first power, which is the integrator's. + /// + /// + /// Rubi's 1.2.1.2 1/((d + e x) (a + b x + c x^2)^4) and x^8/(a + b x + c x^2)^4 + /// ran past a minute: the partial fractions take a symbolic quadratic apart over its + /// roots, and its powers with it. D and Q(p) are named while the reduction + /// runs and written back after, so that each stays one atom. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveARationalFunctionOverAPowerOfASymbolicQuadratic(Entity expr, Entity.Variable x, bool integrateByParts) + { + var (numerator, denominator) = Functions.SingleQuotient.Of(expr); + if (!TreeAnalyzer.TryGetPolynomial(numerator, x, out var top) || top.Keys.Any(k => k.Sign < 0 || !k.CanFitInInt32())) + return null; + Entity? quadratic = null; + var power = 0; + Entity? linear = null; + var linearPower = 0; + Entity constant = Number.Integer.One; + foreach (var factor in Mulf.LinearChildren(denominator)) + { + if (!factor.ContainsNode(x)) + { + constant = constant * factor; + continue; + } + var (@base, whole) = factor is Powf(var b0, Number.Integer { EInteger: var w }) && w.Sign > 0 && w.CanFitInInt32() + ? (b0, w.ToInt32Unchecked()) : (factor, 1); + if (TreeAnalyzer.TryGetPolyLinear(@base, x, out var slope, out _) && !VanishesIdentically(slope)) + { + if (linear is not null && linear != @base) + return null; + linear = @base; + linearPower += whole; + continue; + } + if (quadratic is null && whole >= 2 && TreeAnalyzer.TryGetPolyQuadratic(@base, x, out var leading, out _, out _) && !VanishesIdentically(leading)) + { + quadratic = @base; + power = whole; + continue; + } + return null; + } + if (quadratic is null || !quadratic.Vars.Any(symbol => symbol != x) + || !TreeAnalyzer.TryGetPolyQuadratic(quadratic, x, out var a2, out var a1, out var a0)) + return null; + var discriminant = (4 * a2 * a0 - a1 * a1).InnerSimplified; + if (VanishesIdentically(discriminant)) + return null; + var named = Variable.CreateUnique(expr, "D_q"); + var atTheRoot = Variable.CreateUnique(expr + named, "Q_p"); + var theRoot = Variable.CreateUnique(expr + named + atTheRoot, "x_p"); + Entity d = named; + + static Entity[] Trim(Entity[] poly) + { + var length = poly.Length; + while (length > 0 && poly[length - 1] == Number.Integer.Zero) + length--; + return poly.Take(length).ToArray(); + } + static Entity[] Plus(Entity[] left, Entity[] right) + { + var sum = new Entity[System.Math.Max(left.Length, right.Length)]; + for (var i = 0; i < sum.Length; i++) + sum[i] = ((i < left.Length ? left[i] : Number.Integer.Zero) + (i < right.Length ? right[i] : Number.Integer.Zero)).InnerSimplified; + return sum; + } + static Entity[] Scaled(Entity[] poly, Entity by) => poly.Select(c => (c * by).InnerSimplified).ToArray(); + // Each coefficient in lowest terms over the symbols, so that the next step works on it + // as written small. + static Entity[] Reduced(Entity[] poly) => poly.Select(Functions.PartialFractions.InLowestTermsOverTheSymbols).ToArray(); + + var degree = top.Keys.Count == 0 ? 0 : top.Keys.Max()!.ToInt32Unchecked(); + var start = new Entity[degree + 1]; + for (var i = 0; i <= degree; i++) + start[i] = top.TryGetValue(EInteger.FromInt32(i), out var coefficient) ? coefficient : Number.Integer.Zero; + + // The integrand as a sum of polynomials over L^j Q^k, one per (j, k), merged as they + // arrive so that each stays of small degree. + var terms = new Dictionary<(int Linear, int Quadratic), Entity[]>(); + void Add(int j, int k, Entity[] poly) + => terms[(j, k)] = terms.TryGetValue((j, k), out var already) ? Plus(already, poly) : poly; + Add(linearPower, power, start); + Entity answer = Number.Integer.Zero; + Entity m = Number.Integer.One, p = theRoot; + if (linear is not null && !TreeAnalyzer.TryGetPolyLinear(linear, x, out m!, out _)) + return null; + // Q - Q(p) = (x - p) (c (x + p) + b). + var cofactor = new Entity[] { (a2 * p + a1).InnerSimplified, a2 }; + + while (terms.Count > 0) + { + var (j, k) = terms.Keys.OrderByDescending(key => key.Linear).ThenByDescending(key => key.Quadratic).First(); + var poly = Reduced(terms[(j, k)]); + terms.Remove((j, k)); + if (Trim(poly).Length == 0) + continue; + if (j > 0) + { + // N/(L^j Q^k) = N(p)/(Q(p)^k m^j (x - p)^j) + N1/(m L^(j-1) Q^k) + // - N(p) (c (x + p) + b) sum_i Q(p)^(k-1-i)/(Q(p)^k m L^(j-1) Q^(k-i)), + // with N1 = (N - N(p))/(x - p). + Entity atP = Number.Integer.Zero; + for (var i = poly.Length - 1; i >= 0; i--) + atP = (atP * p + poly[i]).InnerSimplified; + var c = Functions.PartialFractions.InLowestTermsOverTheSymbols(atP / MathS.Pow(atTheRoot, Number.Integer.Create(k)) / MathS.Pow(m, Number.Integer.Create(j))); + answer = answer + (j == 1 ? c * MathS.Ln(linear!) : c * MathS.Pow(linear!, Number.Integer.Create(1 - j)) * MathS.Pow(m, Number.Integer.Create(j - 1)) / (1 - j)); + var n1 = new Entity[System.Math.Max(poly.Length - 1, 0)]; + Entity carry = Number.Integer.Zero; + for (var i = poly.Length - 1; i >= 1; i--) + { + carry = (poly[i] + carry * p).InnerSimplified; + n1[i - 1] = carry; + } + Add(j - 1, k, Scaled(n1, 1 / m)); + for (var i = 0; i < k; i++) + Add(j - 1, k - i, Scaled(cofactor, (-atP * MathS.Pow(atTheRoot, Number.Integer.Create(-1 - i)) / m).InnerSimplified)); + continue; + } + if (k == 0) + { + for (var i = 0; i < poly.Length; i++) + answer = answer + poly[i] * MathS.Pow(x, Number.Integer.Create(i + 1)) / (i + 1); + continue; + } + // P/Q^k: P = S Q + t1 x + t0, and t1 x + t0 = alpha Q' + beta. + var remainder = poly.ToArray(); + var quotient = Enumerable.Repeat((Entity)Number.Integer.Zero, System.Math.Max(remainder.Length - 2, 0)).ToArray(); + for (var i = remainder.Length - 1; i >= 2; i--) + { + var factor = (remainder[i] / a2).InnerSimplified; + quotient[i - 2] = factor; + remainder[i] = Number.Integer.Zero; + remainder[i - 1] = (remainder[i - 1] - factor * a1).InnerSimplified; + remainder[i - 2] = (remainder[i - 2] - factor * a0).InnerSimplified; + } + var t1 = remainder.Length > 1 ? remainder[1] : Number.Integer.Zero; + var t0 = remainder.Length > 0 ? remainder[0] : Number.Integer.Zero; + var alpha = Functions.PartialFractions.InLowestTermsOverTheSymbols(t1 / (2 * a2)); + var beta = Functions.PartialFractions.InLowestTermsOverTheSymbols(t0 - alpha * a1); + Add(0, k - 1, quotient); + if (k == 1) + { + // Over the first power in closed form: alpha ln(Q), and beta times the table's + // integral of 1/Q. Asked of the integrator instead, a linear over Q with these + // coefficients took forty seconds. + if (IntegralPatterns.TryStandardIntegrals(1 / quadratic, x) is not { } overQ) + return null; + answer = answer + alpha * MathS.Ln(quadratic) + beta * overQ; + continue; + } + var lower = MathS.Pow(quadratic, Number.Integer.Create(k - 1)); + answer = answer - alpha / ((k - 1) * lower) + beta * (2 * a2 * x + a1) / ((k - 1) * d * lower); + Add(0, k - 1, new[] { (beta * 2 * a2 * (2 * k - 3) / ((k - 1) * d)).InnerSimplified }); + } + Entity withoutTheLinear = Number.Integer.Zero; + Entity scale = Number.Integer.One; + answer = ((withoutTheLinear + scale * answer) / constant).Substitute(named, discriminant); + if (linear is not null && TreeAnalyzer.TryGetPolyLinear(linear, x, out var mm, out var nn)) + { + var root = (-nn / mm).InnerSimplified; + // Q(p) as one quotient in lowest terms, (c d^2 - b d e + a e^2)/e^2, rather than + // Q written at -d/e wherever it stood. + answer = answer.Substitute(atTheRoot, Functions.PartialFractions.InLowestTermsOverTheSymbols(a2 * root * root + a1 * root + a0)).Substitute(theRoot, root); + } + answer = answer.InnerSimplified; + return Functions.PartialFractions.DerivativeHoldsAtSampledPoints(answer, expr, x) ? answer : null; + } + /// /// A power of a linear beside the derivative of a quadratic, over a whole power of that /// quadratic, by parts once: Q' L^m/Q^n with u = L^m and diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 49f6fbd56..a02c2360c 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) // A power of a linear beside the derivative of a quadratic over a whole power of it, by // parts once, before anything takes the symbolic quadratic apart. if ((answer = IndefiniteIntegralSolver.SolveAPowerOfALinearBesideTheDerivativeOfAQuadraticOverItsPower(expr, x, integrateByParts)) is { }) return answer; + // A rational function over a whole power of a symbolic quadratic, the power lowered one + // at a time, before the partial fractions take the quadratic apart over its roots. + if ((answer = IndefiniteIntegralSolver.SolveARationalFunctionOverAPowerOfASymbolicQuadratic(expr, x, integrateByParts)) is { }) return answer; // A root or a power of a product beside a power of one of its factors, the product // split: `(a + b x^2)^(5/2)/sqrt(a^2 - b^2 x^4)` is `K (a + b x^2)^2/sqrt(a - b x^2)`. // Early, since what reads the product whole is slower and longer: Rubi's 1.2.1.2:2218, diff --git a/Sources/Tests/UnitTests/Calculus/ARationalFunctionOverAPowerOfASymbolicQuadraticIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ARationalFunctionOverAPowerOfASymbolicQuadraticIntegralTest.cs new file mode 100644 index 000000000..6404579ea --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ARationalFunctionOverAPowerOfASymbolicQuadraticIntegralTest.cs @@ -0,0 +1,60 @@ +// +// 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 rational function over a whole power of a symbolic quadratic, beside one linear: the + /// linear taken off at its root and the power of the quadratic lowered one at a time. Rubi's + /// 1.2.1.2 1/((d + e x) (a + b x + c x^2)^4), written here with k for e, + /// which is Euler's number. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class ARationalFunctionOverAPowerOfASymbolicQuadraticIntegralTest + { + private static void DifferentiatesBack(string integrand, params (string, double)[] pins) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + + // Pinned before it is differentiated: the answer is a piecewise on the sign of the + // discriminant, read where it is a number. + 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("x^8/(a+b*x+c*x^2)^4")] + [InlineData("1/((d+k*x)*(a+b*x+c*x^2)^4)")] + [InlineData("1/((d+k*x)^2*(a+c*x^2)^4)")] + public void IsAnsweredAndDifferentiatesBack(string integrand) + { + DifferentiatesBack(integrand, ("a", 2), ("b", 1.5), ("c", 1.25), ("d", 1.25), ("k", 2)); + DifferentiatesBack(integrand, ("a", -1), ("b", 2), ("c", 2), ("d", -0.5), ("k", 1.5)); + } + } +} From 88813cf9b54a92ac090a4af1f8fe276a99ecbd1c Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 15 Oct 2026 06:51:02 +0000 Subject: [PATCH 2/4] Leave a quadratic that factors, and a question below the top, to the partial fractions b^2 - 4ac a square is a quadratic over two linears, which the partial fractions take apart cheaply: (d + e x)^10/((d + e x)(a e + c d x))^4 went from 2 seconds past the budget here. And below a substitution the routes this pre-empts answered shorter. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/IndefiniteIntegralSolver.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 8db434c0d..2388068f9 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -19361,6 +19361,11 @@ Entity TakeOut(Entity node) /// internal static Entity? SolveARationalFunctionOverAPowerOfASymbolicQuadratic(Entity expr, Entity.Variable x, bool integrateByParts) { + // At the question asked only: below a substitution the routes it pre-empts answered + // `(a + b x + c x^2)^3/((1 - d x)^(3/2) (1 + d x)^(3/2))` in 2,917 characters, and + // this in 67,746. + if (!Integration.AnsweringTheQuestionAsked) + return null; var (numerator, denominator) = Functions.SingleQuotient.Of(expr); if (!TreeAnalyzer.TryGetPolynomial(numerator, x, out var top) || top.Keys.Any(k => k.Sign < 0 || !k.CanFitInInt32())) return null; @@ -19400,6 +19405,19 @@ Entity TakeOut(Entity node) var discriminant = (4 * a2 * a0 - a1 * a1).InnerSimplified; if (VanishesIdentically(discriminant)) return null; + // A quadratic that factors over linears is the partial fractions' to take apart, and + // they do it cheaply: `(d + e x)^10/((d + e x) (a e + c d x))^4`, written out, went from + // two seconds past the budget here. + // It does where b^2 - 4 a c is the square of a polynomial in the symbols, which is + // what is asked: the factoring does not find `(d + e x) (a e + c d x)` written out. + var opposite = Functions.PartialFractions.Bare((a1 * a1 - 4 * a2 * a0).Expand().InnerSimplified); + var symbols = opposite.Vars.Distinct().OrderBy(v => v.Name, System.StringComparer.Ordinal).ToArray(); + var indices = new Dictionary(); + for (var i = 0; i < symbols.Length; i++) + indices[symbols[i]] = i; + if (symbols.Length <= MultivariatePolynomial.MaxVariables + && MultivariatePolynomial.TryParse(opposite, indices) is { } parsed && parsed.TrySquareRoot() is not null) + return null; var named = Variable.CreateUnique(expr, "D_q"); var atTheRoot = Variable.CreateUnique(expr + named, "Q_p"); var theRoot = Variable.CreateUnique(expr + named + atTheRoot, "x_p"); From 47a11dac22432443ad82912bd23d25d246c60228 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 15 Oct 2026 09:01:36 +0000 Subject: [PATCH 3/4] Leave a binomial in x^2 and a quadratic over the imaginary unit to the rules before Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Continuous/Integration/IndefiniteIntegralSolver.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 2388068f9..3f60a9126 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -19405,6 +19405,11 @@ Entity TakeOut(Entity node) var discriminant = (4 * a2 * a0 - a1 * a1).InnerSimplified; if (VanishesIdentically(discriminant)) return null; + // Real coefficients only, whose cases the table's 1/Q reads; and with no linear, not a + // binomial in x^2, which the binomial rules answer: `x^22/(a + b x^2)^10` was theirs in + // a second and ten levels of this ran past the budget. + if (Functions.PartialFractions.HoldsTheImaginaryUnit(quadratic) || linear is null && VanishesIdentically(a1)) + return null; // A quadratic that factors over linears is the partial fractions' to take apart, and // they do it cheaply: `(d + e x)^10/((d + e x) (a e + c d x))^4`, written out, went from // two seconds past the budget here. From ed5185534c8dda6671ee05e9e727e91b3b2b6e25 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 15 Oct 2026 12:48:41 +0000 Subject: [PATCH 4/4] Record the answers a power of a symbolic quadratic reduced gives Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 5051710b2..ab2fe2684 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -559,6 +559,23 @@ of the quadratic, parts leaves the quadratic one power lower, which is answered | `"(b + 2*c*x)/((a + b*x + c*x^2)^2*sqrt(d + k*x))".ToEntity().Integrate("x")` | `integral(...)` | 1,075 characters | | `"(b + 2*c*x)*(d + k*x)^(3/2)/(a + b*x + c*x^2)^3".ToEntity().Integrate("x")` | `integral(...)` | 4,054 characters | +### A rational function over a power of a symbolic quadratic is reduced a power at a time + +**Answers where there were none.** `1/((d + k x) (a + b x + c x^2)^4)` ran past a minute, the partial fractions taking the +symbolic quadratic apart over its roots and its powers with them. The linear is taken off at its root now, and the power +of the quadratic is lowered one at a time by the recurrence for the integral of `1/Q^k`, down to the first power, which +is written in closed form. A quadratic that factors over linears is left to the partial fractions +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"x^8/(a + b*x + c*x^2)^4".ToEntity().Integrate("x")` | `integral(...)` | 8,511 characters | +| `"1/((d + k*x)*(a + b*x + c*x^2)^4)".ToEntity().Integrate("x")` | `integral(...)` | 9,091 characters | +| `"1/((d + k*x)^2*(a + b*x + c*x^2)^5)".ToEntity().Integrate("x")` | `integral(...)` | 25,767 characters | +| `"1/((d + k*x)^2*(a + c*x^2)^4)".ToEntity().Integrate("x")` | `integral(...)` | 9,599 characters | +| `"(f + g*x)/((d + k*x)^2*(a + b*x + c*x^2)^3)".ToEntity().Integrate("x")` | `integral(...)` | 21,943 characters | +| `"(A + B*x + C*x^2)/((d + k*x)^3*(a + c*x^2)^3)".ToEntity().Integrate("x")` | `integral(...)` | 25,201 characters | + ### An odd power of a quadratic's derivative over a power of the quadratic is integrated in the quadratic **Shorter answers, and answers where there were none.** `(b + 2c x)^9/(a + b x + c x^2)^3` was answered through the