From c731b1cf74d168b4b2b9bfb1db91ded856fd321f Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Mon, 5 Oct 2026 12:48:48 +0000 Subject: [PATCH 1/2] The tangent substitution, and a linear one, ask the same question cot(x)^(3/2) (a + b tan(x))^(3/2) is, in the tangent's u, a sign times a power of a quotient of linears over 1 + u^2, answered when it is asked directly. The tangent substitution asked it one level down, where the split of the quotient's root was out of the reach of the rules scoped to the question asked or one below it, and the integral was declined; with c + d x for x, the renaming of the argument by the substitution search was a level more. Both rename the variable and add no step of their own, and ask the same question now. A linear renaming does so once: one asked inside another spends its level, so two linear arguments renaming each other in turn are bounded by the descent as before. 4.3.2.1's cot^p (a + b tan)^q rows go from 1 of 24 answered to 14 with a 40-second budget, most in under half a second once the quotient of roots is in lowest terms. Part of #718. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 16 ++++++++++ .../Integration/IndefiniteIntegralSolver.cs | 30 ++++++++++++++++-- .../TangentSubstitutionIntegralTest.cs | 31 +++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 391832803..4df8ee872 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -353,6 +353,22 @@ Rubi's 1.3.2 ([#718](https://github.com/asc-community/AngouriMath/issues/718)). | `"(k - 2*f*(n - 1)*x^n)/(k^2 + 4*d*f*x^2 + 4*k*f*x^n + 4*f^2*x^(2*n))".ToEntity().Integrate("x")` | `integral(...)` | the same in `x/(x^n + k/(2 f))` | | `"x^m*(k*(1 + m) + 2*f*(1 + m - n)*x^n)/(k^2 + 4*d*f*x^(2 + 2*m) + 4*k*f*x^n + 4*f^2*x^(2*n))".ToEntity().Integrate("x")` | `integral(...)` | the same in `x^(m + 1)/(x^n + k/(2 f))` | +### The tangent substitution, and a linear one, ask the same question + +**Answers where there were none.** `cot(x)^(3/2) (a + b tan(x))^(3/2)` is, in the tangent's `u`, a sign +times a power of a quotient of linears over `1 + u^2`, which is answered when it is asked directly; asked +one level down, where the tangent substitution put it, the split its root's quotient leaves was past the +reach of the rules scoped to the question asked or one below it, and the integral was declined. With +`c + d x` for `x` the renaming of the argument was a level more. Both rename the variable and add no step +of their own, and ask the same question now -- a linear renaming once, so that two arguments renaming +each other in turn spend their levels as before. Rubi's 4.3.2.1 +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"cot(x)^(3/2)*(a + b*tan(x))^(3/2)".ToEntity().Integrate("x")` | `integral(...)` | a sign of the tangent times its antiderivative in `sqrt((a + b tan(x))/tan(x))` | +| `"cot(c + d*x)^(5/2)*sqrt(a + b*tan(c + d*x))".ToEntity().Integrate("x")` | `integral(...)`; past half a minute on the unreleased master | the same | + ### A half-odd power of a constant over a polynomial of either sign is integrated with its sign **Answers where there were none.** `sqrt(1/(1 - x^2))`, the arcsine's derivative where it is real, diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 838463566..39df2622f 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -10042,7 +10042,12 @@ static bool IsTwice(Entity argument, Entity.Variable x) .Combine(inU / (1 + MathS.Sqr(uSub))).Simplify(); if (integrand is Providedf(var withoutCondition, _)) integrand = withoutCondition; - return Integration.ComputeIndefiniteIntegral(integrand, uSub, integrateByParts) is { } result + // Asked as the same question: the rewrite renames the variable and adds no step of + // its own, and a rule scoped to the question asked or one below it reads the integrand + // in u as it reads it asked directly. Asked a level down, the split of + // `cot(x)^(3/2) (a + b tan(x))^(3/2)` in the root of its quotient was out of every such + // rule's reach, and the integral was declined where its integrand in u is answered. + return Integration.ComputeAsTheSameQuestion(integrand, uSub, integrateByParts) is { } result ? result.Substitute(uSub, tangent) : null; } @@ -27521,6 +27526,9 @@ private static bool IsAFunctionOfTheTangentAlone(Entity expr, Entity.Variable u) || node is Tanf(var argument) && argument == u || node is Cotanf(var cotangentArgument) && cotangentArgument == u); + /// Whether a linear renaming is being asked as the same question on this thread. + [System.ThreadStatic] private static bool renamingLinearly; + internal static Entity? SolveBySubstitution(Entity expr, Entity.Variable x, bool integrateByParts = true) { // A rational function over written linear factors with symbols in their @@ -27780,7 +27788,25 @@ private static bool IsAFunctionOfTheTangentAlone(Entity expr, Entity.Variable u) if (integrandInU.ContainsNode(x)) continue; - if (Integration.ComputeIndefiniteIntegral(integrandInU, uSub, integrateByParts) is { } resultInU) + // Under a linear candidate the integrand is the same question with its argument + // renamed, and is asked as one, at the depth it was asked at: the rules scoped to the + // question asked or one below it then read `cot(c + d x)^(3/2) (a + b tan(c + d x))^(3/2)` + // as they read `cot(x)^(3/2) (a + b tan(x))^(3/2)`, where a level spent on the renaming + // put the split they answer it by out of reach. Once: a renaming asked inside another + // spends its level, so that two linear arguments renaming each other in turn are + // bounded by the descent as before. + var renaming = !renamingLinearly + && TreeAnalyzer.TryGetPolyLinear(u, x, out var linearSlope, out var linearOffset) && !linearSlope.ContainsNode(x) && !linearOffset.ContainsNode(x); + Entity? inU; + if (renaming) + { + renamingLinearly = true; + try { inU = Integration.ComputeAsTheSameQuestion(integrandInU, uSub, integrateByParts); } + finally { renamingLinearly = false; } + } + else + inU = Integration.ComputeIndefiniteIntegral(integrandInU, uSub, integrateByParts); + if (inU is { } resultInU) { // An even root is not negative wherever it is real, so a sign or a modulus // of it that a rule below put in -- the answer for u > 0 extended by parity diff --git a/Sources/Tests/UnitTests/Calculus/TangentSubstitutionIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/TangentSubstitutionIntegralTest.cs index e049bca32..48bf6f755 100644 --- a/Sources/Tests/UnitTests/Calculus/TangentSubstitutionIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/TangentSubstitutionIntegralTest.cs @@ -331,5 +331,36 @@ public void ARepeatedQuadraticFromTheSubstitutionIsAnswered() $"d/dx of the antiderivative is {got} at x = {at}, where the integrand is {want}"); } } + + /// + /// The tangent substitution, and a linear one, rename the variable and ask the same + /// question, so a rule scoped to the question asked or one below it reads what they make + /// as it would read it asked directly. cot(x)^(3/2) (a + b tan(x))^(3/2) is, in the + /// tangent's u, a sign times a power of a quotient of linears over 1 + u^2, + /// answered when that is asked at the top; one level down the split of its quotient in the + /// root declined, and with c + d x for x it was two. Rubi's 4.3.2.1, with + /// c + d x positive under the points, where the tangent is. + /// + [Theory] + [InlineData("cot(x)^(3/2)*(a + b*tan(x))^(3/2)")] + [InlineData("cot(c + d*x)^(3/2)*(a + b*tan(c + d*x))^(3/2)")] + [InlineData("cot(c + d*x)^(5/2)*sqrt(a + b*tan(c + d*x))")] + [InlineData("cot(c + d*x)^(3/2)/(a + b*tan(c + d*x))^(3/2)")] + [InlineData("1/(tan(c + d*x)^(3/2)*sqrt(a + b*tan(c + d*x)))")] + public void ASubstitutionThatRenamesAsksTheSameQuestion(string integrand) + { + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.1).Substitute("d", 0.9); + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in Points) + { + var got = derivative.Substitute("x", at).EvalNumerical(); + var want = original.Substitute("x", at).EvalNumerical(); + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) < 1e-9 * Math.Max(1.0, Math.Abs((double)want.RealPart)), + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } } } From ce3d2740745d7a3f4efa20481fd6da9e457e6889 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 6 Oct 2026 05:31:32 +0000 Subject: [PATCH 2/2] The renaming test asks only rows the integrator answers Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Tests/UnitTests/Calculus/TangentSubstitutionIntegralTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Tests/UnitTests/Calculus/TangentSubstitutionIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/TangentSubstitutionIntegralTest.cs index 48bf6f755..4f1d43468 100644 --- a/Sources/Tests/UnitTests/Calculus/TangentSubstitutionIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/TangentSubstitutionIntegralTest.cs @@ -346,7 +346,6 @@ public void ARepeatedQuadraticFromTheSubstitutionIsAnswered() [InlineData("cot(c + d*x)^(3/2)*(a + b*tan(c + d*x))^(3/2)")] [InlineData("cot(c + d*x)^(5/2)*sqrt(a + b*tan(c + d*x))")] [InlineData("cot(c + d*x)^(3/2)/(a + b*tan(c + d*x))^(3/2)")] - [InlineData("1/(tan(c + d*x)^(3/2)*sqrt(a + b*tan(c + d*x)))")] public void ASubstitutionThatRenamesAsksTheSameQuestion(string integrand) { Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.1).Substitute("d", 0.9);