diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 936fddfca..94869eaf4 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -1189,6 +1189,29 @@ times a cosine of `u`, which the closed rules answer. Rubi's 4.2.10
| `"x^3*sqrt(a-a*cos(x))".Integrate("x")` | left unevaluated | the antiderivative |
| `"x^2*sqrt(a+a*sin(x))".Integrate("x")` | left unevaluated | the antiderivative |
+### A root of a quadratic in the tangent is rotated until the quadratic has no linear term
+
+`1/sqrt(1 + 2 tan(x) + 3 tan(x)^2)` was left as written. Under `x = y + arctan(m)` the tangent
+becomes `(tan(y) + m)/(1 - m tan(y))` -- the Möbius map that preserves `1 + tan^2`, which is the
+factor the tangent substitution's `dx` brings -- and the quadratic's linear coefficient becomes
+`b(1 - m^2) + 2(c - a)m`, zero for a root of `b m^2 + 2(a - c)m - b`, whose discriminant
+`4((a - c)^2 + b^2)` is never negative. The rotated quadratic `A + C tan(y)^2` goes through the
+tangent substitution to `1/((1 + t^2) sqrt(A + C t^2))`, which was already answered. The radicand
+is *assembled* -- `A = a + bm + cm^2`, `C = am^2 - bm + c` -- rather than substituted into, since
+writing the quotient inside the root leaves a nesting nothing downstream reduces; and a
+`sgn(1 - m tan(y))` comes out in front, the modulus that `sqrt(N/(1 - mS)^2)` leaves.
+
+A numeric rotation only: with symbolic coefficients `m` is a nested surd, the rotated quadratic is
+written in it, and the rational integrator is handed a quotient over that field -- two minutes and
+no answer, where the integrand is declined in under a second unrotated, so the symbolic shape
+keeps its decline ([#718](https://github.com/asc-community/AngouriMath/issues/718)).
+
+| Input | Was (2.5.0) | Now |
+|---|---|---|
+| `"1/sqrt(1+2*tan(x)+3*tan(x)^2)".Integrate("x")` | left unevaluated | the antiderivative, with `sgn(1 - m tan(x - arctan(m)))` in front |
+| `"tan(x)/sqrt(2+tan(x)+tan(x)^2)".Integrate("x")` | left unevaluated | the antiderivative |
+| `"tan(x)^2/(2+tan(x)+tan(x)^2)^(3/2)".Integrate("x")` | left unevaluated | the antiderivative |
+
### `binomial(n, k)` is a function
**Addition, not silent.** The binomial coefficient is a node, `Entity.Binomialf`, spelled
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
index 9a1414b12..d3eff5ea0 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
@@ -14317,6 +14317,120 @@ private static (Entity Factor, Entity Inside)? TryTakeACommonLinearFactorOfASum(
return (factored, inside);
}
+ ///
+ /// A root of a quadratic in tan(x) with a linear term, rotated until it has none:
+ /// 1/sqrt(a + b tan(x) + c tan(x)^2) is 1/sqrt(A + C tan(y)^2) under
+ /// x = y + arctan(m), and the tangent substitution then leaves
+ /// 1/((1 + t^2) sqrt(A + C t^2)), which is answered in closed form.
+ ///
+ ///
+ ///
+ /// tan(y + f) is (tan(y) + m)/(1 - m tan(y)) with m = tan(f), which
+ /// is the Möbius map that preserves 1 + tan^2 -- the factor the tangent
+ /// substitution's dx brings. Under it the quadratic's linear coefficient becomes
+ /// b(1 - m^2) + 2(c - a)m, zero for a root of b m^2 + 2(a - c)m - b, whose
+ /// discriminant 4((a - c)^2 + b^2) is never negative: the two roots are the
+ /// quadratic form's two perpendicular directions and either will do.
+ ///
+ ///
+ /// The radicand is assembled rather than substituted into. Writing
+ /// tan(x) as the quotient inside the root leaves a nested quotient that nothing
+ /// downstream reduces, so the substitution never sees the rotated quadratic; here the
+ /// root becomes N(S)^p (1 - m S)^(-2p) with N(S) = A + C S^2 computed in
+ /// closed form, A = a + b m + c m^2 and C = a m^2 - b m + c. For a half-odd
+ /// power the modulus is what the root leaves -- sqrt(N/(1 - mS)^2) is
+ /// sqrt(N)/|1 - mS| -- so a sgn(1 - m tan(y)) comes out in front, constant
+ /// between its zeros as every such sign in these rules is.
+ ///
+ ///
+ /// The antiderivative is of the rotated integrand, so the answer is it at
+ /// x - arctan(m): a constant shift of the argument, which an antiderivative is
+ /// free to have. Rubi's 4.3.9 and the cotangent shapes the tangent substitution rewrites
+ /// into tangents on the way in.
+ /// https://github.com/asc-community/AngouriMath/issues/718
+ ///
+ ///
+ internal static Entity? SolveByRotatingAwayTheLinearTermInTheTangent(Entity expr, Entity.Variable x, bool integrateByParts)
+ {
+ if (!Integration.AnsweringTheQuestionAskedOrOneBelow)
+ return null;
+ var tangent = MathS.Tan(x);
+ var written = expr.Replace(node => node is Cotanf(var cotangent) && cotangent == x ? 1 / tangent : node);
+ if (!written.ContainsNode(tangent))
+ return null;
+ // A function of the tangent alone: anything else in x is another question.
+ if (written.Replace(node => node == tangent ? Number.Integer.One : node).ContainsNode(x))
+ return null;
+ // One radicand, a quadratic in the tangent with a linear term, to a half-odd power.
+ Entity? a = null, b = null, c = null;
+ var roots = new List<(Entity Node, Number.Rational Power)>();
+ foreach (var node in written.Nodes)
+ {
+ if (node is not Powf(var radicand, Number.Rational power) || power is Number.Integer || !radicand.ContainsNode(x))
+ continue;
+ if (!power.ERational.Denominator.Equals(EInteger.FromInt32(2)))
+ return null;
+ var inT = radicand.Substitute(tangent, Variable.CreateVariableOrConstant("t_rot"));
+ if (inT.ContainsNode(x)
+ || !TreeAnalyzer.TryGetPolynomial(inT, Variable.CreateVariableOrConstant("t_rot"), out var monomials) || monomials.Count == 0
+ || monomials.Keys.Any(k => k.Sign < 0 || k.CompareTo(EInteger.FromInt32(2)) > 0))
+ return null;
+ var thisA = monomials.TryGetValue(EInteger.Zero, out var a0) ? a0 : Number.Integer.Zero;
+ var thisB = monomials.TryGetValue(EInteger.One, out var b1) ? b1 : Number.Integer.Zero;
+ var thisC = monomials.TryGetValue(EInteger.FromInt32(2), out var c2) ? c2 : Number.Integer.Zero;
+ if (a is not null && (a != thisA || b != thisB || c != thisC))
+ return null;
+ (a, b, c) = (thisA, thisB, thisC);
+ roots.Add((node, power));
+ }
+ if (a is null || b is null || c is null || roots.Count == 0
+ || TreeAnalyzer.IsZero(b) || b.Evaled is Number.Complex { IsZero: true })
+ return null;
+
+ // m = ((c - a) + sqrt((a - c)^2 + b^2))/b, a root of b m^2 + 2(a - c) m - b.
+ var m = ((c - a + MathS.Sqrt((MathS.Sqr(a - c) + MathS.Sqr(b)).InnerSimplified)) / b).InnerSimplified;
+ // A number: with symbolic coefficients `m` is a nested surd, the rotated quadratic's
+ // are written in it, and what the substitution hands the rational integrator is a
+ // quotient over that field -- two minutes and no answer, where the integrand is
+ // declined in a tenth of a second unrotated. The rotation is exact for any real
+ // coefficients and this is a bound on the work, not on the mathematics.
+ if (m.Evaled is not Number.Real { IsFinite: true })
+ return null;
+ var rotatedTangent = ((tangent + m) / (1 - m * tangent)).InnerSimplified;
+ var scale = (1 - m * tangent).InnerSimplified;
+ var rotatedA = (a + b * m + c * MathS.Sqr(m)).InnerSimplified;
+ var rotatedC = (a * MathS.Sqr(m) - b * m + c).InnerSimplified;
+ var quadratic = (rotatedA + rotatedC * MathS.Sqr(tangent)).InnerSimplified;
+
+ // Each root out of the way first, so that the tangents inside it are not rewritten
+ // twice: the radicand's rotation is the closed form above, not the substitution.
+ var placeholders = new List<(Variable Symbol, Entity Value)>();
+ var skeleton = written;
+ Entity signs = Number.Integer.One;
+ foreach (var (node, power) in roots)
+ {
+ var symbol = Variable.CreateUnique(written, "r_rot" + placeholders.Count);
+ skeleton = skeleton.Substitute(node, symbol);
+ // (N/(1 - mS)^2)^(p/2) is N^(p/2) |1 - mS|^(-p), and |z|^(-p) is sgn(z)^p z^(-p).
+ var numerator = Number.Integer.Create(power.ERational.Numerator);
+ if (!numerator.EInteger.IsEven)
+ signs = signs * MathS.Signum(scale);
+ placeholders.Add((symbol, MathS.Pow(quadratic, power) * MathS.Pow(scale, (-numerator).InnerSimplified)));
+ }
+ if (skeleton.ContainsNode(x) && !skeleton.ContainsNode(tangent))
+ return null;
+ var rotated = skeleton.Substitute(tangent, rotatedTangent);
+ foreach (var (symbol, value) in placeholders)
+ rotated = rotated.Substitute(symbol, value);
+ rotated = rotated.InnerSimplified;
+ if (rotated.ContainsNode(x) is false || rotated.Nodes.Any(node => node == MathS.NaN))
+ return null;
+ if (Integration.ComputeAsAQuestionOfItsOwn(rotated, x, integrateByParts) is not { } answer)
+ return null;
+ var shifted = (signs == Number.Integer.One ? answer : signs * answer).Substitute(x, (x - MathS.Arctan(m)).InnerSimplified);
+ return shifted.Nodes.Any(node => node == MathS.NaN) ? null : shifted;
+ }
+
///
/// A pair of hyperbolic powers two apart whose coefficients kill the reduction's
/// residual: cosh(y)^p - (p - 1)/p cosh(y)^(p - 2) is
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
index fafb4772a..d50b5b83e 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
@@ -654,6 +654,10 @@ private static Entity Normalized(Entity expr, Entity.Variable x) =>
// alone is both rules' and the reduction's answer for it is shorter.
if ((answer = IndefiniteIntegralSolver.SolveByTrigonometricPowerSubstitution(expr, x)) is { }) return answer;
if ((answer = IndefiniteIntegralSolver.SolveByTangentSubstitution(expr, x, integrateByParts)) is { }) return answer;
+ // A root of a quadratic in the tangent with a linear term, rotated until it has
+ // none: `1/sqrt(a + b tan(x) + c tan(x)^2)` is `1/sqrt(A + C tan(y)^2)` under
+ // `x = y + arctan(m)`, which the substitution above then answers.
+ if ((answer = IndefiniteIntegralSolver.SolveByRotatingAwayTheLinearTermInTheTangent(expr, x, integrateByParts)) is { }) return answer;
// And the logarithm's own substitution, beside the tangent's. It goes the other way
// -- `x = e^u`, so it *introduces* an exponential rather than cancelling one -- which
// is why the general substitution does not find it and why it pays: the integrator
diff --git a/Sources/Tests/UnitTests/Calculus/MixedTrigonometricArgumentsTest.cs b/Sources/Tests/UnitTests/Calculus/MixedTrigonometricArgumentsTest.cs
index b5bac263c..0a334d6ae 100644
--- a/Sources/Tests/UnitTests/Calculus/MixedTrigonometricArgumentsTest.cs
+++ b/Sources/Tests/UnitTests/Calculus/MixedTrigonometricArgumentsTest.cs
@@ -186,5 +186,40 @@ public void ARootOfAProductOfTwoFunctionsIsNotLetThrough(string integrand)
var integral = integrand.ToEntity().Integrate("x");
Assert.DoesNotContain("NaN", integral.Stringize());
}
+
+ ///
+ /// A root of a quadratic in the tangent with a linear term, rotated until it has none:
+ /// x = y + arctan(m) with m a root of b m^2 + 2(a - c) m - b makes
+ /// a + b tan(x) + c tan(x)^2 into A + C tan(y)^2, and the tangent
+ /// substitution then leaves 1/((1 + t^2) sqrt(A + C t^2)), which is answered.
+ /// The radicand is assembled in closed form: substituting the Möbius quotient into it
+ /// leaves a nesting that nothing downstream reduces.
+ /// #718
+ ///
+ [Theory]
+ [InlineData("1/sqrt(1 + 2*tan(x) + 3*tan(x)^2)")]
+ [InlineData("1/sqrt(2 + tan(x) + tan(x)^2)")]
+ [InlineData("1/sqrt(3 - 2*tan(x) + tan(x)^2)")]
+ [InlineData("tan(x)/sqrt(2 + tan(x) + tan(x)^2)")]
+ [InlineData("tan(x)^2/(2 + tan(x) + tan(x)^2)^(3/2)")]
+ public void AQuadraticInTheTangentIsRotatedUntilItHasNoLinearTerm(string integrand)
+ => DifferentiatesBack(integrand);
+
+ ///
+ /// With symbolic coefficients the rotation is a nested surd, the rotated quadratic is
+ /// written in it, and what the rational integrator is handed is a quotient over that
+ /// field: two minutes and no answer, where the integrand is declined in a moment
+ /// unrotated. The rule asks for a numeric rotation, and this pins that the verdict for
+ /// the symbolic shape is still a quick decline rather than a search.
+ ///
+ [Fact]
+ public void ASymbolicQuadraticInTheTangentIsDeclinedAndNotSearched()
+ {
+ var watch = System.Diagnostics.Stopwatch.StartNew();
+ var integral = "1/sqrt(a + b*tan(x) + c*tan(x)^2)".ToEntity().Integrate("x");
+ watch.Stop();
+ Assert.Contains("integral(", integral.Stringize());
+ Assert.True(watch.Elapsed < IntegrationDecline.Guard, $"the symbolic shape took {watch.Elapsed.TotalSeconds:F1} s");
+ }
}
}