From a34b2201043d2ff08973e47f937d38a0b220a09a Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 9 Oct 2026 21:18:20 +0000 Subject: [PATCH 1/3] A symbolic quadratic with a square discriminant, split into its linears before the division Part of #718. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/IndefiniteIntegralSolver.cs | 87 ++++++++++++++++--- .../ASymbolicQuadraticSplitIntegralTest.cs | 50 +++++++++++ 2 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/ASymbolicQuadraticSplitIntegralTest.cs diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 009dc96c6..0cea53a3a 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -724,6 +724,17 @@ 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. + if (WithSymbolicQuadraticsInAPowerSplit(denominator, x, 1) is { } overLinears + && Patterns.GatherPowersOfOneBase(WithSumsAsPolynomialsInTheSymbols(numerator, x) / 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 +761,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 +778,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 +15052,32 @@ private static bool IsASumOfMonomials(Entity expr, Entity.Variable x) return true; } + /// + /// 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 +15092,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 +15141,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 +15159,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}"); + } + } +} From d8fbd230a63cf51b0fba5606aedda5636c198038 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 9 Oct 2026 22:45:24 +0000 Subject: [PATCH 2/3] Its breaking-changes entry Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 From b3e84d05a8f1faa5918e5313d08b7a3b6e81ee25 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 9 Oct 2026 23:42:05 +0000 Subject: [PATCH 3/3] Only where a polynomial numerator shares one of the linears Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/IndefiniteIntegralSolver.cs | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 0cea53a3a..76c5c9b42 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -729,8 +729,14 @@ Entity Back(Entity mapped) // 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 - && Patterns.GatherPowersOfOneBase(WithSumsAsPolynomialsInTheSymbols(numerator, x) / overLinears) is var overTheSplit + && 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; @@ -15052,6 +15058,28 @@ 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