From 0829f697deff5e8315f4aa220b962ae073012bce Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 16 Oct 2026 18:28:02 +0000 Subject: [PATCH 1/2] Integrate a polynomial over a symbolic quartic that splits into two quadratics in a hidden variable A palindromic quartic c x^4 + b x^3 + e x^2 + b x + c is c (x^2 - p x + 1)(x^2 - q x + 1), p and q the roots of c w^2 + b w + e - 2 c, being x^2 times a quadratic in w = x + 1/x; one whose odd terms vanish under y = x + c3/(4 c4) is c4 (y^2 - p)(y^2 - q). The integral over the factored quartic is asked with p and q named and they are written back. Only a quartic written as one sum is read: the factored one is palindromic again, and read again the answer ran to 803,488 characters. Rubi's 1.3.1 (A + B x + C x^2 + D x^3)/(a + b x + c x^2 + b x^3 + a x^4) and 1/(4 a c + 4 c^2 x^2 + 4 c d x^3 + d^2 x^4) were declined. Part of https://github.com/asc-community/AngouriMath/issues/718 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/IndefiniteIntegralSolver.cs | 87 +++++++++++++++++++ .../Integration/Integration.Definition.cs | 3 + ...ThatSplitsIntoTwoQuadraticsIntegralTest.cs | 65 ++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/ASymbolicQuarticThatSplitsIntoTwoQuadraticsIntegralTest.cs diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 44973d73f..b41d405db 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -29184,6 +29184,93 @@ bool HoldsItsValue(Entity factor) => Functions.PartialFractions.HoldsAtSampledPoints(factor, factor.Substitute(x, Number.Rational.Create(3, 10)), x); } + /// + /// A polynomial over a quartic with symbols in it that splits into two quadratics in a hidden + /// variable: a palindromic one, c x^4 + b x^3 + e x^2 + b x + c, is + /// c (x^2 - p x + 1)(x^2 - q x + 1) with p and q the roots of + /// c w^2 + b w + e - 2 c, since it is x^2 times a quadratic in w = x + 1/x; + /// and one whose odd terms vanish under y = x + c3/(4 c4) is c4 (y^2 - p)(y^2 - q). + /// The integral over the factored quartic is asked with p and q named, and they are + /// written back. + /// + /// + /// The integral over two real quadratics splits on the signs of their discriminants, which mean + /// nothing for roots that may be complex; the first arm differentiates back for any of them, and + /// the answer is checked. A quartic already even is the biquadratic rule's. Rubi's 1.3.1 + /// (A + B x + C x^2 + D x^3)/(a + b x + c x^2 + b x^3 + a x^4) and + /// 1/(4 a c + 4 c^2 x^2 + 4 c d x^3 + d^2 x^4) were declined. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveAPolynomialOverASymbolicQuarticThatSplitsIntoTwoQuadratics(Entity expr, Entity.Variable x, bool integrateByParts) + { + if (!Integration.AnsweringTheQuestionAsked || !TryReadAsQuotient(expr, out var numerator, out var denominator) + || !denominator.Vars.Any(v => v != x)) + return null; + // A quartic written as one sum only: the factored one asked below is palindromic again once + // multiplied out, and read as one the rule split it again, four levels deep. + if (Mulf.LinearChildren(denominator).Count(factor => factor.ContainsNode(x)) != 1 + || Mulf.LinearChildren(denominator).First(factor => factor.ContainsNode(x)) is not (Sumf or Minusf)) + return null; + if (!TreeAnalyzer.TryGetPolynomial(denominator, x, out var read) || read.Keys.Any(key => key.Sign < 0 || key.CompareTo(EInteger.FromInt32(4)) > 0) + || !read.ContainsKey(EInteger.FromInt32(4)) || read.Values.Any(value => value.ContainsNode(x)) + || !TreeAnalyzer.TryGetPolynomial(numerator, x, out var readAbove) || readAbove.Values.Any(value => value.ContainsNode(x))) + return null; + Entity At(int k) => read.TryGetValue(EInteger.FromInt32(k), out var value) ? value : Number.Integer.Zero; + var (c0, c1, c2, c3, c4) = (At(0), At(1), At(2), At(3), At(4)); + if (VanishesIdentically(c1.Expand()) && VanishesIdentically(c3.Expand())) + return null; + var p = Variable.CreateUnique(expr, "p_quart"); + var q = Variable.CreateUnique(expr + p, "q_quart"); + Entity first, second, pValue, qValue; + if (VanishesIdentically((c0 - c4).Expand()) && VanishesIdentically((c1 - c3).Expand())) + { + var discriminant = (c3 * c3 - 4 * c4 * (c2 - 2 * c4)).Expand().InnerSimplified; + if (Functions.PartialFractions.IsZeroAsAValue(discriminant)) + return null; + pValue = (-c3 + MathS.Sqrt(discriminant)) / (2 * c4); + qValue = (-c3 - MathS.Sqrt(discriminant)) / (2 * c4); + (first, second) = (MathS.Sqr(x) - p * x + 1, MathS.Sqr(x) - q * x + 1); + } + else + { + var shift = Functions.PartialFractions.InLowestTermsOverTheSymbols(c3 / (4 * c4)); + // The coefficients in y = x + s by the binomial theorem, d_k = sum_j c_j C(j, k) (-s)^(j - k), + // each in lowest terms over the symbols: the shift puts `c/d` into them, and `d^2 (c/d)^3` + // beside `c^3/d` is not seen to cancel as written. + var coefficients = new[] { c0, c1, c2, c3, c4 }; + int[,] binomial = { { 1, 0, 0, 0, 0 }, { 1, 1, 0, 0, 0 }, { 1, 2, 1, 0, 0 }, { 1, 3, 3, 1, 0 }, { 1, 4, 6, 4, 1 } }; + Entity Shifted(int k) + { + Entity sum = Number.Integer.Zero; + for (var j = k; j <= 4; j++) + sum += coefficients[j] * binomial[j, k] * MathS.Pow(-shift, Number.Integer.Create(j - k)); + return Functions.PartialFractions.InLowestTermsOverTheSymbols(sum); + } + if (!Functions.PartialFractions.IsZeroAsAValue(Shifted(1)) || !Functions.PartialFractions.IsZeroAsAValue(Shifted(3))) + return null; + var e2 = Functions.PartialFractions.InLowestTermsOverTheSymbols(Shifted(2) / c4); + var e0 = Functions.PartialFractions.InLowestTermsOverTheSymbols(Shifted(0) / c4); + var discriminant = (e2 * e2 - 4 * e0).Expand().InnerSimplified; + if (Functions.PartialFractions.IsZeroAsAValue(discriminant)) + return null; + pValue = (-e2 + MathS.Sqrt(discriminant)) / 2; + qValue = (-e2 - MathS.Sqrt(discriminant)) / 2; + (first, second) = (MathS.Sqr(x + shift) - p, MathS.Sqr(x + shift) - q); + } + if (Integration.ComputeAsAQuestionOfItsOwn(numerator / (c4 * first * second), x, integrateByParts) is not { } inPQ) + return null; + inPQ = inPQ.Replace(node => node switch + { + Piecewise piecewise when piecewise.Cases.Any() => piecewise.Cases.First().Expression, + Providedf provided => provided.Expression, + _ => node, + }); + // The roots written in without simplifying after: simplified, the answer to + // `1/(a + b x + c x^2 + b x^3 + a x^4)` multiplied out to 370,884 characters from about one thousand. + var answer = inPQ.InnerSimplified.Substitute(p, pValue).Substitute(q, qValue); + return Functions.PartialFractions.DerivativeDiffersWhereReal(answer, expr, x) ? null : answer; + } + /// /// A power of a constant multiple of a radicand that a logarithm holds under a root, /// written over that radicand: acosh(c x) is ln(c x + sqrt(c^2 x^2 - 1)), diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 176741cf2..592a7a7d8 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -664,6 +664,9 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // A sum free of the variable that comes to a number multiplied out, as that number, // before anything reads a coefficient or an exponent in it: `2 + 2 a - 2 (1 + a)`. if ((answer = IndefiniteIntegralSolver.SolveByFoldingConstantSumsToTheirNumbers(expr, x, integrateByParts)) is { }) return answer; + // A polynomial over a symbolic quartic that splits into two quadratics in a hidden variable, + // palindromic or even after a shift: the rules below spent seconds declining both. + if ((answer = IndefiniteIntegralSolver.SolveAPolynomialOverASymbolicQuarticThatSplitsIntoTwoQuadratics(expr, x, integrateByParts)) is { }) return answer; // A function of an inverse of something not linear times its derivative up to a factor // constant though written with x: `atan(c x/sqrt(a - c^2 x^2))^2/sqrt(d - c^2 d x^2/a)`. Early, // since it fires only where that factor holds x, and the rules before it spent 28 seconds on diff --git a/Sources/Tests/UnitTests/Calculus/ASymbolicQuarticThatSplitsIntoTwoQuadraticsIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ASymbolicQuarticThatSplitsIntoTwoQuadraticsIntegralTest.cs new file mode 100644 index 000000000..395222263 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ASymbolicQuarticThatSplitsIntoTwoQuadraticsIntegralTest.cs @@ -0,0 +1,65 @@ +// +// 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 symbolic quartic that splits into two quadratics in a hidden variable: a + /// palindromic one over x + 1/x, and one even after a shift. Rubi's 1.3.1 + /// (A + B x + C x^2 + D x^3)/(a + b x + c x^2 + b x^3 + a x^4) and + /// 1/(4 a c + 4 c^2 x^2 + 4 c d x^3 + d^2 x^4). + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class ASymbolicQuarticThatSplitsIntoTwoQuadraticsIntegralTest + { + private static void DifferentiatesBack(string integrand, params (string, double)[] pins) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + + 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}"); + } + } + + // d with the sign that keeps the argument real at every sampled x, and |d| below 2/2.3^2 + // so that the inverse sine and cosine stay inside [-1, 1]; off it the exact checks hold too. + [Theory] + [InlineData("(A+B*x+G*x^2+D*x^3)/(a+b*x+c*x^2+b*x^3+a*x^4)")] + [InlineData("1/(a+b*x+c*x^2+b*x^3+a*x^4)")] + [InlineData("1/(4*a*c+4*c^2*x^2+4*c*d*x^3+d^2*x^4)")] + [InlineData("x/(4*a*c+4*c^2*x^2+4*c*d*x^3+d^2*x^4)")] + public void IsAnsweredAndDifferentiatesBack(string integrand) + { + var integral = integrand.ToEntity().Integrate("x").Stringize(); + // The factored quartic asked below is palindromic again; read as a fresh one, the answer + // ran to 803,488 characters. + Assert.True(integral.Length < 10_000, $"{integral.Length} characters"); + DifferentiatesBack(integrand, ("A", 0.6), ("B", 1.25), ("G", 0.3), ("D", 0.75), ("a", 1.4), ("b", 0.5), ("c", 3), ("d", 2)); + DifferentiatesBack(integrand, ("A", -1.5), ("B", 0.25), ("G", -0.4), ("D", -2), ("a", 0.5), ("b", -3), ("c", -1), ("d", -0.3)); + } + } +} From b1aeac7845afb8e1b40917067911babbd1c311d9 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 17 Oct 2026 00:38:54 +0000 Subject: [PATCH 2/2] Record the symbolic quartic split in the breaking changes Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index f0c95a6fc..69f8bbf7d 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -55,6 +55,22 @@ numbers for every coefficient the solve is now done in exact rationals, to degre | `"1/((3-2*x+x^2)^(11/2)*(1+x+2*x^2)^5)".ToEntity().Integrate("x")` | `integral(...)` | 1,853 characters | | `"1/((3-2*x+x^2)^(21/2)*(1+x+2*x^2)^10)".ToEntity().Integrate("x")` | `integral(...)` | 3,475 characters | +### A polynomial over a symbolic quartic that is two quadratics in a hidden variable is split over them + +**Answers where there were none.** `1/(a + b x + c x^2 + b x^3 + a x^4)` was declined, and so were Rubi's 1.3.1 +`(A + B x + C x^2 + D x^3)/(a + b x + c x^2 + b x^3 + a x^4)` and `1/(4 a c + 4 c^2 x^2 + 4 c d x^3 + d^2 x^4)`. A +palindromic quartic is `x^2` times a quadratic in `x + 1/x`, and a quartic whose odd terms vanish under +`y = x + c/d` is a quadratic in `y^2`; either is the product of two quadratics in x whose +coefficients are the roots of that quadratic, and the polynomial over it is split over the two +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/(a+b*x+c*x^2+b*x^3+a*x^4)".ToEntity().Integrate("x")` | `integral(...)` | 1,620 characters | +| `"(A+B*x+C*x^2+D*x^3)/(a+b*x+c*x^2+b*x^3+a*x^4)".ToEntity().Integrate("x")` | `integral(...)` | 3,416 characters | +| `"1/(4*a*c+4*c^2*x^2+4*c*d*x^3+d^2*x^4)".ToEntity().Integrate("x")` | `integral(...)` | 3,920 characters | +| `"x/(4*a*c+4*c^2*x^2+4*c*d*x^3+d^2*x^4)".ToEntity().Integrate("x")` | `integral(...)` | 2,315 characters | + ### `|` is divisibility, and was disjunction **Read this one first.** It is the only entry here that changes what existing input *means* rather