From 5c645ed99497897af90d710512a0b514e220a866 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 23 Sep 2026 03:22:31 +0000 Subject: [PATCH] A root of a quadratic in the tangent is rotated until the quadratic has no linear term MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `1/sqrt(1 + 2 tan(x) + 3 tan(x)^2)` was left as written, though `1/((1 + t^2) sqrt(A + C t^2))` -- what the tangent substitution leaves of it once the linear term is gone -- was already answered. Under `x = y + arctan(m)` the tangent becomes `(tan(y) + m)/(1 - m tan(y))`, the Möbius map that preserves `1 + tan^2`, 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, so the rotation is always real. The two roots are the quadratic form's perpendicular directions and either will do. The radicand is assembled -- `A = a + bm + cm^2`, `C = am^2 - bm + c` -- and not substituted into: writing the quotient inside the root leaves a nesting that nothing downstream reduces, which is how the first version of this rule turned a decline into a ninety-second search. The modulus that `sqrt(N/(1 - mS)^2)` leaves comes out in front as `sgn(1 - m tan(y))`, constant between its zeros. A numeric rotation only. With symbolic coefficients `m` 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 under a second unrotated. That bound is on the work rather than on the mathematics, and a test pins that the symbolic shape is still declined quickly. Family 4 of the Rubi suite: 290/422 either way, 0 wrong -- its rows of this family are symbolic; family 6 377/417 and the 1774-problem suite 1707 unchanged. Suite 12655 passed; allocation gate passed on all 19 gated benchmarks. Each new row differentiates back at five points. Part of #718. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 23 ++++ .../Integration/IndefiniteIntegralSolver.cs | 114 ++++++++++++++++++ .../Integration/Integration.Definition.cs | 4 + .../MixedTrigonometricArgumentsTest.cs | 35 ++++++ 4 files changed, 176 insertions(+) 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"); + } } }