diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 5e09bede5..99f3ca9db 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -208,6 +208,19 @@ after it
|---|---|---|
| `"(a*c + b*c*x)^(-3-2*p)*(f + g*x)*(a^2 + 2*a*b*x + b^2*x^2)^p".ToEntity().Integrate("x")` | `integral(...)`; an answer with no value on the unreleased master | the antiderivative |
+### A rational function of the secant with two sums below the bar is written in the cosine
+
+**Answers where there were none.** `sec(x)/((a + b sec(x)) (c + d sec(x))^2)` ran past thirty seconds: the
+half-angle substitution read the secant's spelling into a rational function of the half-angle tangent that a
+substitution search expanded without end. As one quotient in the cosine it is
+`cos(x)^2/((a cos(x) + b) (c cos(x) + d)^2)`, answered in a second, and it is written so where two sums in the
+secant stand below the bar and none above ([#718](https://github.com/asc-community/AngouriMath/issues/718)).
+
+| Input | Was (2.5.0) | Now |
+|---|---|---|
+| `"sec(x)/((a + b*sec(x))*(c + d*sec(x))^2)".ToEntity().Integrate("x")` | `integral(...)`; past thirty seconds on the unreleased master | 9,439 characters, a case for each sign of two discriminants |
+| `"sec(x)/((a + b*sec(x))^2*(c + d*sec(x)))".ToEntity().Integrate("x")` | `integral(...)`; past thirty seconds on the unreleased master | 9,430 characters |
+
### `a + i a tan` of a shifted linear below the bar is integrated in the linear
**Answers where there were none.** `sqrt(a + i a tan(g + f x)) (A + B tan(g + f x))/sqrt(c - i c tan(g + f x))`
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
index 800082a70..46840f353 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
@@ -27741,6 +27741,58 @@ bool IsAPolynomialInBoth(Entity polynomial)
return null;
}
+ ///
+ /// A rational function of the secant of one argument, or of the cosecant, written in the
+ /// cosine, or the sine, as one quotient: sec(z)/((a + b sec(z)) (c + d sec(z))^2) is
+ /// cos(z)^2/((a cos(z) + b) (c cos(z) + d)^2) wherever the secant is defined, exactly.
+ ///
+ ///
+ /// The half-angle substitution read the secant's spelling into a rational function of the
+ /// half-angle tangent that a substitution search expanded past thirty seconds, where the
+ /// cosine's is answered in half of one. Only for two sums or more, all below the bar. Rubi's 4.5.2.3.
+ /// https://github.com/asc-community/AngouriMath/issues/718
+ ///
+ internal static Entity? SolveByWritingARationalFunctionOfTheSecantInTheCosine(Entity expr, Entity.Variable x, bool integrateByParts)
+ {
+ Entity? reciprocal = null;
+ foreach (var node in expr.Nodes)
+ {
+ if (!node.ContainsNode(x) || node is Entity.Variable)
+ continue;
+ switch (node)
+ {
+ case Secantf or Cosecantf:
+ if (reciprocal is null)
+ reciprocal = node;
+ else if (reciprocal != node)
+ return null;
+ break;
+ case Sumf or Minusf or Mulf or Divf or Powf(_, Number.Integer):
+ break;
+ default:
+ // Inside the reciprocal's own argument, a linear in x.
+ if (reciprocal is not null && reciprocal.DirectChildren.First().Nodes.Contains(node))
+ break;
+ if (expr.Nodes.Any(other => other is Secantf or Cosecantf && other.DirectChildren.First().Nodes.Contains(node)))
+ break;
+ return null;
+ }
+ }
+ if (reciprocal is null || !TreeAnalyzer.TryGetPolyLinear(reciprocal.DirectChildren.First(), x, out _, out _))
+ return null;
+ // Only two sums in the secant or more, all below the bar: one, or one above, the rules for
+ // the secant answer as written, and in the cosine `sec(x)^5/(a + b sec(x))^3` ran past five
+ // seconds where it is half of one.
+ var (above, below) = Functions.SingleQuotient.Of(expr);
+ bool ASum(Entity node) => node is Sumf or Minusf && node.ContainsNode(reciprocal);
+ if (above.Nodes.Any(ASum) || below.Nodes.Where(ASum).Distinct().Count() < 2)
+ return null;
+ var argument = reciprocal.DirectChildren.First();
+ var function = reciprocal is Secantf ? MathS.Cos(argument) : MathS.Sin(argument);
+ var rewritten = Functions.SingleQuotient.Combine(expr.Replace(node => node == reciprocal ? 1 / function : node));
+ return Integration.ComputeAsTheSameQuestion(rewritten, x, integrateByParts);
+ }
+
///
/// A half-odd power of the secant or the cosecant, in an integrand with the cosine or the
/// sine of the same argument in it, written as a power of the cosine or the sine:
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
index 8192a0c89..93817f696 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
@@ -1101,6 +1101,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) =>
// Bioche's first two rules before the third: a rational function of the two that
// is odd in one of them is a rational function of the other, smaller than the
// half-angle tangent's and answered more shortly.
+ // A rational function of the secant alone in the cosine first, as one quotient: the
+ // half-angle tangent of the secant's spelling was a search past thirty seconds.
+ if ((answer = IndefiniteIntegralSolver.SolveByWritingARationalFunctionOfTheSecantInTheCosine(expr, x, integrateByParts)) is { }) return answer;
if ((answer = IndefiniteIntegralSolver.SolveByBiochesOddSubstitution(expr, x, integrateByParts)) is { }) return answer;
if ((answer = IndefiniteIntegralSolver.SolveByHalfAngleSubstitution(expr, x, integrateByParts)) is { }) return answer;
// The substitution by a sine or a cosine where an odd power of the complement is
diff --git a/Sources/Tests/UnitTests/Calculus/ARationalFunctionOfTheSecantInTheCosineIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ARationalFunctionOfTheSecantInTheCosineIntegralTest.cs
new file mode 100644
index 000000000..68ff1a5ea
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/ARationalFunctionOfTheSecantInTheCosineIntegralTest.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 rational function of the secant with two sums in it below the bar, written in the cosine as
+ /// one quotient: sec(x)/((a + b sec(x)) (c + d sec(x))^2) is
+ /// cos(x)^2/((a cos(x) + b) (c cos(x) + d)^2). Rubi's 4.5.2.3.
+ /// #718
+ ///
+ [Trait("Area", "Calculus")]
+ public sealed class ARationalFunctionOfTheSecantInTheCosineIntegralTest
+ {
+ [Theory]
+ [InlineData("sec(x)/((a + b*sec(x))*(c + d*sec(x))^2)")]
+ [InlineData("sec(x)/((a + b*sec(x))^2*(c + d*sec(x)))")]
+ public void AsOneQuotient(string integrand)
+ {
+ var integral = integrand.ToEntity().Integrate("x");
+ var text = integral.Stringize();
+ Assert.DoesNotContain("integral(", text);
+ Assert.True(text.Length < 20000, $"{text.Length} characters of answer for {integrand}");
+ Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 2.1).Substitute("d", 0.6);
+ var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x");
+ var original = Pinned(integrand.ToEntity());
+ var compared = 0;
+ foreach (var at in new[] { -1.2, -0.7, 0.3, 0.8, 1.3, 2.9 })
+ {
+ 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}");
+ }
+ }
+}