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))")]