From 0e700ca77e96f951495c66fe076b4f1830adbe2a Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 3 Oct 2026 14:00:37 +0000 Subject: [PATCH 1/3] A linear substitution's quotient is not simplified The substitution search reaches tan(g + h x) through u = g + h x and simplified the quotient of the integrand by du/dx in u before asking for its integral. Under a linear candidate that quotient is the integrand with its argument renamed, over a constant, and holds nothing to simplify; with the nine symbols of Rubi's 4.3.4.2 in it the simplification took 28 of 32 s. It is handed on as it is: (a + b tan(g + h x))(A + B tan(g + h x) + K tan(g + h x)^2)/(c + d tan(g + h x))^2 is answered in 4.5 s where master took 38, and the cube over a cube in 18 s where master ran past a minute and a half. Part of #718. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 14 +++++ .../Integration/IndefiniteIntegralSolver.cs | 11 +++- .../LinearArgumentSubstitutionTest.cs | 54 +++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 Sources/Tests/UnitTests/Calculus/LinearArgumentSubstitutionTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 0f1e70dfd..0b1d32d34 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -651,6 +651,20 @@ taken apart into pieces each closed the same way | `"(2 + x)/((2 + 4*x - 3*x^2)*(1 + 3*x + 2*x^2)^(3/2))".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative | | `"1/((x^2 + 1)*sqrt(x^2 + x + 1))".ToEntity().Integrate("x")` | `integral(...)` | a logarithm and an arctangent | +### A rational function of the tangent of a linear argument with symbols in it is not simplified under the linear substitution + +**Answers where there were none.** The substitution search reaches `tan(g + h x)` through `u = g + h x`, +and simplified the quotient of the integrand by `h` in `u` before asking for its integral: with the +nine symbols of Rubi's 4.3.4.2 in it that took most of a minute, for an expression that is the +integrand with its argument renamed and holds nothing to simplify. Under a linear candidate the +quotient is handed on as it is +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"(a + b*tan(g + h*x))*(A + B*tan(g + h*x) + K*tan(g + h*x)^2)/(c + d*tan(g + h*x))^2".ToEntity().Integrate("x")` | `integral(...)` after 33 s | the antiderivative, in 4.5 s | +| `"(c + d*tan(g + h*x))^3*(A + B*tan(g + h*x) + K*tan(g + h*x)^2)/(a + b*tan(g + h*x))^3".ToEntity().Integrate("x")` | no answer within a minute and a half | the antiderivative, in 18 s | + ### A power of a multiple of a quadratic's derivative beside a power of the quadratic is a binomial **Improvement, not silent.** `(b d + 2 c d x)^m (a + b x + c x^2)^p`, with a power that is not whole, diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 99a46ed02..d442401a5 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -23252,7 +23252,16 @@ private static bool HoldsAnEvenRootBesides(Entity expr, Entity radicand, Entity. && expr.Complexity <= (expr.Vars.Any(v => v != x) ? LargestSymbolicIntegrandCollected : LargestIntegrandOfferedSums) ? WithThePowersOfXCollected(Functions.SingleQuotient.Combine(expr / duDx), x) : source / duDx; - integrandInU = SimplifiedWithoutTheImaginaryUnit(InTermsOf(quotient, u, uSub, x), expr); + // Under a linear candidate the quotient is the integrand with its argument + // renamed, over a constant, and there is nothing in it for the simplifier to + // find: `(a + b tan(e + f x))(A + B tan(e + f x) + C tan(e + f x)^2)/(c + d tan(e + f x))^2` + // under `u = e + f x` spent 28 of its 32 s being simplified, nine symbols and + // nothing to cancel. + var inTermsOfU = InTermsOf(quotient, u, uSub, x); + integrandInU = !inTermsOfU.ContainsNode(x) + && TreeAnalyzer.TryGetPolyLinear(u, x, out var slope, out var offset) && !slope.ContainsNode(x) && !offset.ContainsNode(x) + ? Functions.PartialFractions.Bare(inTermsOfU.InnerSimplified) + : SimplifiedWithoutTheImaginaryUnit(inTermsOfU, expr); // A factor written on both sides of the bar cancelled, where x survived: // the one-level simplification leaves `u/((a w + b)^2 p u)` as it is, and // the candidate was refused for the u it did not cancel. diff --git a/Sources/Tests/UnitTests/Calculus/LinearArgumentSubstitutionTest.cs b/Sources/Tests/UnitTests/Calculus/LinearArgumentSubstitutionTest.cs new file mode 100644 index 000000000..73c596001 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/LinearArgumentSubstitutionTest.cs @@ -0,0 +1,54 @@ +// +// 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 tangent of a linear argument with symbols for its + /// coefficients, Rubi's 4.3.3.1 and 4.3.4.2. The substitution search reaches the tangent + /// through u = g + h x, and under a linear candidate the quotient by its derivative is + /// the integrand with its argument renamed over a constant: there is nothing in it to + /// simplify, and simplifying it with nine symbols in it was most of the time each of these + /// took. + /// #718 + /// + /// + /// Checked by differentiating back with the symbols pinned after the integral is taken, at + /// points where the tangent and every factor below the bar stay away from their poles and + /// zeros. + /// + [Trait("Area", "Calculus")] + public sealed class LinearArgumentSubstitutionTest + { + private static readonly double[] Points = { 0.2, 0.5, 0.8 }; + + [Theory] + [InlineData("(a + b*tan(g + h*x))*(A + B*tan(g + h*x) + K*tan(g + h*x)^2)/(c + d*tan(g + h*x))^2")] + [InlineData("(a + b*tan(g + h*x))*(A + B*tan(g + h*x))/(c + d*tan(g + h*x))^2")] + [InlineData("(A + B*tan(g + h*x) + K*tan(g + h*x)^2)/((a + b*tan(g + h*x))*(c + d*tan(g + h*x)))")] + public void ARationalFunctionOfTheTangentOfALinear(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Entity Pin(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 2.1).Substitute("d", 0.9) + .Substitute("g", 0.3).Substitute("h", 1.1).Substitute("A", 0.5).Substitute("B", 1.7).Substitute("K", 0.4); + var derivative = Pin(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pin(integrand.ToEntity()); + foreach (var point in Points) + { + var expected = original.Substitute("x", point).EvalNumerical().RealPart.EDecimal.ToDouble(); + var actual = derivative.Substitute("x", point).EvalNumerical().RealPart.EDecimal.ToDouble(); + Assert.True(Math.Abs(expected - actual) < 1e-8 * Math.Max(1, Math.Abs(expected)), + $"d/dx of the antiderivative of {integrand} is {actual} at x = {point}, where the integrand is {expected}"); + } + } + } +} From 3dfbcf5fef2f5061298352bbd3f62dc7a4819a02 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 3 Oct 2026 19:08:01 +0000 Subject: [PATCH 2/3] Only a function of the tangent alone is handed on unsimplified under a linear substitution Measured one build at a time, seven integrands master answers ran out of time on the branch: beside a sine, a secant, a logarithm or the imaginary unit, the simplified form is what the rules after the substitution read, and cot(c + d x)^8/(a + a sin(c + d x)) went from one second to seventy without it. The quotient is handed on unsimplified only where every function of u in it is its tangent or cotangent and it holds no imaginary unit, which is 4.3.4.2's shape; the seven are back at master's times and 4.3.4.2's answers keep theirs. Part of #718. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 5 +++-- .../Integration/IndefiniteIntegralSolver.cs | 21 ++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 0b1d32d34..f5f309140 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -656,8 +656,9 @@ taken apart into pieces each closed the same way **Answers where there were none.** The substitution search reaches `tan(g + h x)` through `u = g + h x`, and simplified the quotient of the integrand by `h` in `u` before asking for its integral: with the nine symbols of Rubi's 4.3.4.2 in it that took most of a minute, for an expression that is the -integrand with its argument renamed and holds nothing to simplify. Under a linear candidate the -quotient is handed on as it is +integrand with its argument renamed and holds nothing to simplify. Under a linear candidate, where +the integrand in `u` is a function of the tangent alone and holds no imaginary unit, the quotient +is handed on as it is ([#718](https://github.com/asc-community/AngouriMath/issues/718)). | Input | Was (2.5.0) | Now | diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index d442401a5..d0a5479d1 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -23101,6 +23101,17 @@ private static bool HoldsAnEvenRootBesides(Entity expr, Entity radicand, Entity. return false; } + /// + /// Whether every function of in is its tangent + /// or its cotangent: sums, products, quotients and powers of those, and of anything free of + /// . + /// + private static bool IsAFunctionOfTheTangentAlone(Entity expr, Entity.Variable u) + => expr.Nodes.All(node => node is Sumf or Minusf or Mulf or Divf or Powf or Number or Variable + || !node.ContainsNode(u) + || node is Tanf(var argument) && argument == u + || node is Cotanf(var cotangentArgument) && cotangentArgument == u); + internal static Entity? SolveBySubstitution(Entity expr, Entity.Variable x, bool integrateByParts = true) { // A rational function over written linear factors with symbols in their @@ -23253,13 +23264,17 @@ private static bool HoldsAnEvenRootBesides(Entity expr, Entity radicand, Entity. ? WithThePowersOfXCollected(Functions.SingleQuotient.Combine(expr / duDx), x) : source / duDx; // Under a linear candidate the quotient is the integrand with its argument - // renamed, over a constant, and there is nothing in it for the simplifier to - // find: `(a + b tan(e + f x))(A + B tan(e + f x) + C tan(e + f x)^2)/(c + d tan(e + f x))^2` + // renamed, over a constant, and where it is a function of the tangent alone + // there is nothing in it for the simplifier to find: + // `(a + b tan(e + f x))(A + B tan(e + f x) + C tan(e + f x)^2)/(c + d tan(e + f x))^2` // under `u = e + f x` spent 28 of its 32 s being simplified, nine symbols and - // nothing to cancel. + // nothing to cancel. Only there: beside a sine, a secant, a logarithm or the + // imaginary unit the simplified form is what the rules after this read, and + // `cot(c + d x)^8/(a + a sin(c + d x))` went from one second to seventy without it. var inTermsOfU = InTermsOf(quotient, u, uSub, x); integrandInU = !inTermsOfU.ContainsNode(x) && TreeAnalyzer.TryGetPolyLinear(u, x, out var slope, out var offset) && !slope.ContainsNode(x) && !offset.ContainsNode(x) + && IsAFunctionOfTheTangentAlone(inTermsOfU, uSub) && !HoldsTheImaginaryUnit(inTermsOfU) ? Functions.PartialFractions.Bare(inTermsOfU.InnerSimplified) : SimplifiedWithoutTheImaginaryUnit(inTermsOfU, expr); // A factor written on both sides of the bar cancelled, where x survived: From a7c010057b26015a0478aba0cb005accb0538143 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 4 Oct 2026 04:40:31 +0000 Subject: [PATCH 3/3] The linear substitution's entry gives the time master measures today Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 081901e92..ec3ab26df 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -830,7 +830,7 @@ those now. Each answer holds on both sides of zero, an odd root of a negative be **Answers where there were none.** The substitution search reaches `tan(g + h x)` through `u = g + h x`, and simplified the quotient of the integrand by `h` in `u` before asking for its integral: with the -nine symbols of Rubi's 4.3.4.2 in it that took most of a minute, for an expression that is the +nine symbols of Rubi's 4.3.4.2 in it that took about half a minute, for an expression that is the integrand with its argument renamed and holds nothing to simplify. Under a linear candidate, where the integrand in `u` is a function of the tangent alone and holds no imaginary unit, the quotient is handed on as it is