diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 531381a24..88eeae61c 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -678,6 +678,21 @@ was declined. Each of its terms alone is not elementary, and the sum is the deri
|---|---|---|
| `"e^(a+b*x)*sinh(c+d*x)^(3/2)-3/4*e^(a+b*x)*d^2/((b^2-9/4*d^2)*sqrt(sinh(c+d*x)))".ToEntity().Integrate("x")` | `integral(...)` | 249 characters |
+### Under the half tangent, a factor the parts share is written out
+
+**Answers where there were none, shorter than master's.** Rubi's 6.7.1 `sech(x)^2/(a + c sech(x) + b tanh(x))` ran past
+the corpus's budget. Master answered it in 1,383,216 characters after twenty-two seconds. Under `u = tanh(x/2)` the parts
+of the integrand are homogenised apart, and a factor they share stayed hidden inside the expanded quartic of one of them:
+`(1 + u^2)^2` over `(1 + u^2)^2` times a quartic that holds `1 + u^2` once more. The factors `1 + u^2`, `1 - u^2`, `1 - u`, `1 + u`
+and `u`, which the homogenising makes, are now divided out of the written factors of the denominator and out of the
+numerator, and the integrand is `1/((1 + u^2) Q(u))` for a quadratic Q
+([#718](https://github.com/asc-community/AngouriMath/issues/718)).
+
+| Input | Was (2.5.0) | Now |
+|---|---|---|
+| `"sech(x)^2/(a+c*sech(x)+b*tanh(x))".ToEntity().Integrate("x")` | `integral(...)` | 2,931 characters, where before this change master gave 1,383,216 |
+| `"csch(x)^2/(a+c*csch(x)+b*coth(x))".ToEntity().Integrate("x")` | `integral(...)` | 1,766 characters, where before this change master gave 1,690 |
+
### A function of the hyperbolic tangent alone is integrated under `u = tanh`
**Answers where there were none.** `tanh(x)/sqrt(a + b tanh(x)^2 + c tanh(x)^4)` was declined, and so were 35 more of
diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs
index a920387b4..6243fcad4 100644
--- a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs
+++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs
@@ -882,6 +882,87 @@ private static Entity WithThePrimitiveDenominator(Entity quotient)
return primitive.IsConstant ? scaledTop : scaledTop / primitive.ToEntity(variables);
}
+ ///
+ /// over , a product of powers as
+ /// written, with each of divided out of every written factor of
+ /// the denominator as often as it goes exactly and written as a power of its own, and then
+ /// out of the numerator as often as it goes there too. Null where no candidate divides a
+ /// factor it is not already, where none then divides the numerator, or where a side does not
+ /// read as a polynomial over the rationals.
+ ///
+ ///
+ /// A substitution homogenises the parts of an integrand apart, and a factor they share is
+ /// then hidden inside the expanded quartic of one of them: under u = tanh(x/2),
+ /// sech(x)^2/(a + c sech(x) + b tanh(x)) is (1 + u^2)^2 over (1 + u^2)^2
+ /// times a quartic that holds 1 + u^2 once more. Written out, it is 1/((1 + u^2) Q(u))
+ /// with Q a quadratic, which the rational rules read.
+ /// https://github.com/asc-community/AngouriMath/issues/718
+ ///
+ internal static Entity? WithTheCandidateFactorsWrittenOut(Entity numerator, Entity denominator, IReadOnlyList candidates)
+ {
+ var variables = numerator.Vars.Concat(denominator.Vars).Concat(candidates.SelectMany(c => c.Vars)).Distinct()
+ .OrderBy(v => v.Name, System.StringComparer.Ordinal).ToArray();
+ if (variables.Length == 0 || variables.Length > MultivariatePolynomial.MaxVariables)
+ return null;
+ var indices = new Dictionary(variables.Length);
+ for (var i = 0; i < variables.Length; i++)
+ indices[variables[i]] = i;
+ var parsed = new List();
+ foreach (var candidate in candidates)
+ if (MultivariatePolynomial.TryParse(candidate, indices) is { IsConstant: false } read)
+ parsed.Add(read);
+ else
+ return null;
+ var times = new int[parsed.Count];
+ var changed = false;
+ Entity rest = Integer.One;
+ foreach (var written in Mulf.LinearChildren(denominator))
+ {
+ var (@base, power) = written is Powf(var repeated, Integer { EInteger.Sign: > 0 } raised) && raised.EInteger.CanFitInInt32()
+ ? (repeated, raised.EInteger.ToInt32Unchecked()) : (written, 1);
+ if (!@base.Vars.Any() || MultivariatePolynomial.TryParse(@base, indices) is not { IsConstant: false } factor)
+ {
+ rest *= written;
+ continue;
+ }
+ var divided = false;
+ for (var k = 0; k < parsed.Count; k++)
+ while (factor.DivideExact(parsed[k]) is { } quotient)
+ {
+ // A factor that is the candidate itself, to a power, is already written out.
+ divided |= !quotient.IsConstant;
+ factor = quotient;
+ times[k] += power;
+ }
+ changed |= divided;
+ var remaining = factor.ToEntity(variables);
+ if (remaining != Integer.One)
+ rest *= power == 1 ? remaining : MathS.Pow(remaining, power);
+ }
+ if (!changed)
+ return null;
+ if (MultivariatePolynomial.TryParse(numerator, indices) is not { } top)
+ return null;
+ // Only where the numerator shares one of them: written out with nothing cancelled, the
+ // tangent's `(c + d x)^3/(a + a tanh(e + f x))^3` ran past the budget it was answered in.
+ var cancelled = false;
+ for (var k = 0; k < parsed.Count; k++)
+ while (times[k] > 0 && top.DivideExact(parsed[k]) is { } quotient)
+ {
+ top = quotient;
+ times[k]--;
+ cancelled = true;
+ }
+ if (!cancelled)
+ return null;
+ Entity below = rest;
+ for (var k = 0; k < parsed.Count; k++)
+ if (times[k] > 0)
+ below *= times[k] == 1 ? candidates[k] : MathS.Pow(candidates[k], times[k]);
+ var above = top.ToEntity(variables);
+ return below == Integer.One ? above : above / below;
+ }
+
///
/// over , a product of
/// powers as written, with every written factor that divides the numerator exactly
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
index 91b4b1ad6..817c6afcb 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
@@ -34467,6 +34467,15 @@ private static bool IsTheZeroPolynomial(Entity expr)
&& Mulf.LinearChildren(below).All(factor => !factor.ContainsNode(t) || factor == t || factor is Powf(var b, Number.Integer) && b == t)))
continue;
var rationalInT = WithThePowersOfOnePlusMinusTCollected(inT / (1 - MathS.Sqr(t)));
+ // With the factors the parts were homogenised over written out of the rest: `sech(x)^2`
+ // and `a + c sech(x) + b tanh(x)` are each over `1 + t^2`, and the one in the quartic
+ // below was hidden, where the rational integrator wrote 1.38 million characters.
+ // `1 - t^2` before its two linears, so that an integrand even in t is written so: split
+ // into them, `sinh(u)^8/(a - b sinh(u)^4)^2` took ten times the memory.
+ if (Functions.SingleQuotient.Of(rationalInT) is var (aboveT, belowT) && belowT.ContainsNode(t)
+ && Functions.PartialFractions.WithTheCandidateFactorsWrittenOut(aboveT, belowT,
+ new Entity[] { 1 + MathS.Sqr(t), 1 - MathS.Sqr(t), 1 - t, 1 + t, t }) is { } writtenOut)
+ rationalInT = writtenOut;
if (rationalInT.ContainsNode(square) || rationalInT.ContainsNode(vt) || rationalInT.Nodes.Any(node => node == MathS.NaN)
|| Integration.ComputeAsAQuestionOfItsOwn(rationalInT, t, integrateByParts) is not { } evenResult)
return null;
diff --git a/Sources/Tests/UnitTests/Calculus/TheHalfTangentsFactorsWrittenOutIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/TheHalfTangentsFactorsWrittenOutIntegralTest.cs
new file mode 100644
index 000000000..b0463c78d
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/TheHalfTangentsFactorsWrittenOutIntegralTest.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
+{
+ ///
+ /// Under u = tanh(x/2) the parts of an integrand are homogenised apart, and a factor they
+ /// share was left hidden inside the expanded quartic of one of them: Rubi's 6.7.1
+ /// sech(x)^2/(a + c sech(x) + b tanh(x)) is (1 + u^2)^2 over (1 + u^2)^2 times a
+ /// quartic holding 1 + u^2 once more, and the rational integrator wrote 1.38 million
+ /// characters in twenty-two seconds, past the corpus's budget. Written out, it is
+ /// 1/((1 + u^2) Q(u)) for a quadratic Q.
+ /// #718
+ ///
+ [Trait("Area", "Calculus")]
+ public sealed class TheHalfTangentsFactorsWrittenOutIntegralTest
+ {
+ [Theory]
+ // Rubi 6.7.1:910.
+ [InlineData("sech(x)^2/(a + c*sech(x) + b*tanh(x))")]
+ [InlineData("csch(x)^2/(a + c*csch(x) + b*coth(x))")]
+ public void IsAnsweredShortly(string integrand)
+ {
+ var integral = integrand.ToEntity().Integrate("x");
+ var text = integral.Stringize();
+ Assert.DoesNotContain("integral(", text);
+ Assert.True(text.Length < 5000, $"{integrand} came back in {text.Length} characters");
+ Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4);
+ var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x");
+ var original = Pinned(integrand.ToEntity());
+ foreach (var at in new[] { -2.2, -0.9, 0.3, 1.4, 2.1 })
+ {
+ var want = original.Substitute("x", at).EvalNumerical();
+ var got = derivative.Substitute("x", at).EvalNumerical();
+ Assert.False(got.IsNaN, $"d/dx of the antiderivative of {integrand} has no value at x = {at}, where the integrand is {want}");
+ 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}");
+ }
+ }
+ }
+}