From a78552d4e4abcb8e8d862139011169cfee7ee872 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 20 Oct 2026 22:04:51 +0000 Subject: [PATCH] Leave a hyperbolic rational function to the second pass where its question in s would need a sum over roots #1961 asks a hyperbolic integrand as a trigonometric one in s. In the first pass, where the exponential substitution has declined because a sum over roots would be needed, the question in s took 2.5-7 s and declined too, and the second pass then answered in e^x as before: 1/(1 + sinh(x)^3) went from 0.21 GB to 4.98 GB for the same answer. With rationalToo set in the first pass and a sum over roots wanted, the rule now declines when the question in s is rational in cos(s) or sin(s) alone and either its denominator holds odd powers of the sine at degree 3 or more, or it has an irreducible factor of degree past 2 over Q (in y, or in y^2 where only even powers stand), which survives t = tan(s/2) or tan(s). 1/(1 + sinh(x)^3), 1/(1 - sinh(x)^3), 1/(1 + sinh(x)^5), 1/(1 + sinh(x)^8), 1/(1 + cosh(x)^8), 1/(1 - cosh(x)^5): the answers before #1961, character for character, at their cost before it. #1961's 30 gained rows answer as on master, and the seven where it wrote a closed form in place of a sum over roots keep it (6.1.7:305/314/318, 6.2.7:101/102/107, 6.7.1:1173). Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/IndefiniteIntegralSolver.cs | 57 ++++++++++++++++++- ...ntegrandAsATrigonometricOneIntegralTest.cs | 22 ++++++- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index a229ea7eb..7c9aa60b4 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -1125,7 +1125,11 @@ private static bool IsADerivativeOfAFactorBelowTheBar(Entity expr, Entity.Variab /// e^L, which the exponential substitution answers in real terms; with it true, only those, /// after that substitution has declined them. Asked only of the question asked, and last: the /// question in s is not cheap to decline, and asked early it took time from the rules - /// that answer x/cosh(x)^(7/2) + 3/5 x sqrt(cosh(x)) and the like. + /// that answer x/cosh(x)^(7/2) + 3/5 x sqrt(cosh(x)) and the like. Nor in the first pass + /// where the exponential substitution wants a sum over roots and the question in s, a + /// rational function of cos(s) or sin(s) alone, wants one too, by a factor below + /// the bar of degree above two: the second pass answers it in e^L, and the question in + /// s took seconds to decline, five gigabytes for 1/(1 + sinh(x)^3) against 0.2. /// internal static Entity? SolveAHyperbolicIntegrandAsATrigonometricOne(Entity expr, Entity.Variable x, bool integrateByParts, bool rationalToo) { @@ -1190,6 +1194,10 @@ private static bool IsADerivativeOfAFactorBelowTheBar(Entity expr, Entity.Variab }); if (!sawASineOrCosine || inS.ContainsNode(w)) return null; + // Wanting a sum over roots in e^L and, by the denominator, in s too: the second pass's. + if (rationalToo && Integration.ASumOverRootsWouldAnswer && !Integration.SumsOverRootsAllowed + && AFactorAboveAQuadraticInS(inS, s)) + return null; var question = MathS.i / slopeOfL * inS; if (Integration.ComputeAsAQuestionOfItsOwn(question, s, integrateByParts) is not { } inTermsOfS || inTermsOfS.Nodes.Any(node => node is Integralf || node == MathS.NaN)) @@ -1209,6 +1217,53 @@ private static bool IsADerivativeOfAFactorBelowTheBar(Entity expr, Entity.Variab }).Substitute(s, -MathS.i * linear).InnerSimplified; return Functions.PartialFractions.DerivativeHoldsAtSampledPoints(answer, expr, x) ? answer : null; + // Whether inS, rational in cos(s) alone or sin(s) alone, has a denominator of degree three or + // more that t = tan(s/2) or t = tan(s) leaves with an irreducible factor of degree above two, + // which the rational rules write only as a sum over roots. The cosine, and the square of the + // sine, are quotients of linears in t^2 there, so a factor over the rationals keeps its + // degree: 1 + cos(s)^8 keeps 1 + v^4, and 1 - sin(s)^8 splits. A sine to odd powers is + // 2t/(1 + t^2), not a function of t^2, and the cubics and quintics in it all declined. + static bool AFactorAboveAQuadraticInS(Entity inS, Variable s) + { + foreach (var circular in new Entity[] { MathS.Cos(s), MathS.Sin(s) }) + { + var y = Variable.CreateUnique(inS, "y_osb"); + var inY = inS.Replace(node => node == circular ? y : node); + if (inY.ContainsNode(s) || !inY.ContainsNode(y) || !IsARationalFunction(inY, y)) + continue; + if (!TreeAnalyzer.TryGetPolynomial(Functions.SingleQuotient.Of(inY).Denominator, y, out var read)) + return false; + var terms = read.Where(term => term.Value.Evaled is not Number.Complex { IsZero: true }).ToList(); + if (terms.Any(term => term.Key.Sign < 0) || !terms.Any(term => term.Key.CompareTo(EInteger.FromInt32(3)) >= 0)) + return false; + if (terms.Any(term => !term.Key.IsEven)) + return circular is Sinf || AFactorAboveAQuadratic(terms, halved: false); + return AFactorAboveAQuadratic(terms, halved: true); + } + return false; + + // Over the rationals; false where a coefficient is not rational, as nothing is known then. + static bool AFactorAboveAQuadratic(List> terms, bool halved) + { + var rationals = new Dictionary(); + var denominator = EInteger.One; + foreach (var term in terms) + { + var (power, coefficient) = (term.Key, term.Value); + if (coefficient.Evaled is not Number.Rational rational || !power.CanFitInInt32() || power.ToInt32Checked() > 2 * IntegerPolynomial.MaxDegree) + return false; + rationals[halved ? power.ToInt32Checked() / 2 : power.ToInt32Checked()] = rational.ERational; + denominator = denominator * rational.ERational.Denominator / denominator.Gcd(rational.ERational.Denominator); + } + var whole = new EInteger[rationals.Keys.Max() + 1]; + for (var k = 0; k < whole.Length; k++) + whole[k] = rationals.TryGetValue(k, out var c) ? c.Numerator * (denominator / c.Denominator) : EInteger.Zero; + return IntegerPolynomial.Create(whole) is { Degree: > 2 and <= IntegerPolynomial.MaxDegree } polynomial + && Functions.PolynomialFactorization.FactorPrimitive(polynomial.PrimitivePart()) is { } parts + && parts.Any(part => part.Factor.Degree > 2); + } + } + // +1 for W^2 + 1 and -1 for W^2 - 1, as polynomials in W; null for anything else. static int? Squared(Entity polynomial, Variable w) { diff --git a/Sources/Tests/UnitTests/Calculus/AHyperbolicIntegrandAsATrigonometricOneIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/AHyperbolicIntegrandAsATrigonometricOneIntegralTest.cs index 9bfa27355..c7eb7d6ce 100644 --- a/Sources/Tests/UnitTests/Calculus/AHyperbolicIntegrandAsATrigonometricOneIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/AHyperbolicIntegrandAsATrigonometricOneIntegralTest.cs @@ -31,7 +31,9 @@ public sealed class AHyperbolicIntegrandAsATrigonometricOneIntegralTest [InlineData("sinh(x)/(a + b*sinh(x) + c*sinh(x)^2)")] // Rubi's 6.5.3. [InlineData("coth(a + b*x)/sqrt(c + b*sech(a + b*x))")] - public void AsTheTrigonometricQuestion(string integrand) + public void AsTheTrigonometricQuestion(string integrand) => AnsweredAndCheckedBack(integrand); + + private static string AnsweredAndCheckedBack(string integrand) { var integral = integrand.ToEntity().Integrate("x"); var text = integral.Stringize(); @@ -53,8 +55,26 @@ public void AsTheTrigonometricQuestion(string integrand) $"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}"); + return text; } + // Wanting a sum over roots in e^x, these are asked in s only where s does not want one too: + // there the trigonometric answer stands, in closed form. + [Theory] + [InlineData("1/(1 + sinh(x)^4)")] + [InlineData("1/(1 + cosh(x)^3)")] + [InlineData("sech(x)^2/(3 - 4*tanh(x)^3)")] + public void KeepsTheClosedFormWhereSFactorsIntoQuadratics(string integrand) + => Assert.DoesNotContain("sum(", AnsweredAndCheckedBack(integrand)); + + // And where s wants one too, as for a cubic in sin(s) or 1 + cos(s)^8, the second pass answers + // in e^x, as before the rule. + [Theory] + [InlineData("1/(1 + sinh(x)^3)")] + [InlineData("1/(1 + cosh(x)^8)")] + public void LeavesTheSumOverRootsToTheSecondPass(string integrand) + => Assert.Contains("sum(", AnsweredAndCheckedBack(integrand)); + // Asked before the rules in x, the question in s took the time they answer these in. [Theory] [InlineData("x/cosh(x)^(7/2) + 3/5*x*sqrt(cosh(x))")]