From 0bf5cb94391085f1c22793d1cd6dffce32eeddc0 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 20 Oct 2026 17:43:27 +0000 Subject: [PATCH] Integrate a rational function of the tangent beside roots of linears in it Under t = tan(z), dz = dt/(f (1 + t^2)), and a rational function of the tangent beside roots of linears in it is a rational function of t beside roots of linears in t. It is now integrated term by term over the partial fractions of its rational part, with the 1 + t^2 below the bar written as the linears of its roots, (1 + i t)(1 - i t). Each term is the roots over one power of a linear, which the rules for a root of a linear over a linear answer. Rubi's 4.3.4.2 (c + d tan(z))^(5/2) (A + B tan(z) + C tan(z)^2)/sqrt(a + b tan(z)) and six more of its rows, and three of 4.3.2.1, were past the budget. Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 19 ++ .../Integration/IndefiniteIntegralSolver.cs | 168 ++++++++++++++++++ .../Integration/Integration.Definition.cs | 4 + ...entBesideRootsOfLinearsInItIntegralTest.cs | 59 ++++++ 4 files changed, 250 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/ARationalFunctionOfTheTangentBesideRootsOfLinearsInItIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 671eff440..f17cfdfe0 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -3189,6 +3189,25 @@ were read as numbers only. In `w = e^(i z)` the integral is a power of `w` besid | `"(k*sec(c + d*x))^(-4 - n)*(a + i*a*tan(c + d*x))^n".ToEntity().Integrate("x")` | `integral(...)` | the integrand times powers of `e^(i (c + d x))` from `n - 4` to `n + 4`, each over its exponent | | `"(a + i*a*tan(c + d*x))^n/(k*sec(c + d*x))^n".ToEntity().Integrate("x")` | `integral(...)` | the integrand over `i d n` | +### A rational function of the tangent beside roots of linears in it is integrated over the linears of `1 + tan^2` + +**Answers where there were none.** `(c + d tan(z))^(5/2) (A + B tan(z) + C tan(z)^2)/sqrt(a + b tan(z))` and six more of +Rubi's 4.3.4.2, and `1/((a + i a tan(z))^3 (c + d tan(z))^(3/2))` and two more of 4.3.2.1, were declined. In +`t = tan(z)`, under the root of the quotient of the two linears or the root of the one, the `1 + t^2` that `dz` puts +below the bar is a quartic with symbols in every coefficient. It is now written as its linears `(1 + i t)(1 - i t)` and +the rational part split over them, so that each term is the roots over one power of a linear, which the rules for a +root of a linear over a linear answer; the sum is checked by its derivative in `t`. Only two roots of linears with +distinct roots, beside a polynomial or a linear below the bar, or one root below the bar beside a whole power of +`a ± i a tan(z)`. Their neighbours that the unreleased master answered come through it as well, shorter and sooner +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"(c + d*tan(g + h*x))^(5/2)*(A + B*tan(g + h*x) + C*tan(g + h*x)^2)/sqrt(a + b*tan(g + h*x))".ToEntity().Integrate("x")` | `integral(...)` | 4,315 characters | +| `"(A + B*tan(g + h*x) + C*tan(g + h*x)^2)/((a + b*tan(g + h*x))^(3/2)*(c + d*tan(g + h*x))^(3/2))".ToEntity().Integrate("x")` | `integral(...)` | 11,405 characters | +| `"1/((a + i*a*tan(g + h*x))^3*(c + d*tan(g + h*x))^(3/2))".ToEntity().Integrate("x")` | `integral(...)` | 6,498 characters | +| `"(c + d*tan(g + h*x))^(5/2)*(A + B*tan(g + h*x) + C*tan(g + h*x)^2)/(a + b*tan(g + h*x))^(5/2)".ToEntity().Integrate("x")` | `integral(...)`; 41,892 characters, in 10 s, on the unreleased master | 29,265 characters, in 1.4 s | + ### A rational function of the tangent beside a power of `a + i a tan` is integrated in that sum **Answers where there were none.** `(A + B tan(c + d x))/sqrt(a + i a tan(c + d x))` and the rest of diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index c91066d1e..8edbb879d 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -33505,6 +33505,174 @@ static bool IsAWholeNumberOfSteps(ERational difference) return Integration.ComputeAsAQuestionOfItsOwn(expanded.Expand().InnerSimplified, x, integrateByParts); } + /// + /// A rational function of tan(z) beside roots of linears in it, integrated in + /// t = tan(z) term by term over the partial fractions of its rational part, with the + /// 1 + t^2 that dz = dt/(f (1 + t^2)) puts below the bar written as the linears + /// of its roots, (1 + i t)(1 - i t): each term is the roots over one power of a linear, + /// which the rules for a root of a linear over a linear answer. + /// + /// + /// + /// Rubi's 4.3.4.2 (c + d tan(z))^(5/2) (A + B tan(z) + C tan(z)^2)/sqrt(a + b tan(z)) and + /// 4.3.2.1 1/((a + i a tan(z))^3 (c + d tan(z))^(3/2)) ran past the budget, as their + /// neighbours with smaller powers did not: in t, under the root of the quotient of the two + /// linears or the root of the one, 1 + t^2 is a quartic with symbols in every coefficient. + /// Over its linears the same integrands are three terms, or a few more, of a second or less each. + /// + /// + /// Only two roots of linears with distinct roots beside a polynomial or a linear below the + /// bar, or one root below the bar beside a whole power of a +- i a tan(z) below it, which + /// shares a root with 1 + t^2; neither root a root of t or of 1 +- i t, which + /// the rules for a root of the tangent and for the imaginary sum read, and below the bar linears + /// only. Two roots alone, and a root above the bar beside the imaginary sum, the substitution in + /// the root answers in as few terms and as soon, and are left to it. The decomposition is exact, + /// its coefficients constants in the symbols, and the sum is checked by its derivative in + /// t before it is returned: each term's answer is its own rule's. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + /// + internal static Entity? SolveARationalFunctionOfTheTangentBesideRootsOfLinearsInIt(Entity expr, Entity.Variable x, bool integrateByParts) + { + Entity? argument = null; + foreach (var node in expr.Nodes) + { + if (!node.ContainsNode(x)) + continue; + if (node is Sinf or Cosf or Secantf or Cosecantf) + return null; + if (node is not (Tanf or Cotanf)) + continue; + var inner = node.DirectChildren.First(); + if (argument is null) + argument = inner; + else if (argument != inner) + return null; + } + if (argument is null || !TreeAnalyzer.TryGetPolyLinear(argument, x, out var rate, out _) + || rate.ContainsNode(x) || VanishesIdentically(rate)) + return null; + var t = Variable.CreateUnique(expr, "t_tangent"); + var inT = expr.Replace(node => node switch + { + Tanf(var y) when y == argument => t, + Cotanf(var y) when y == argument => 1 / t, + _ => node, + }); + if (inT.ContainsNode(x)) + return null; + + // `A + c t` with `c = +-i A`, which vanishes at a root of `1 + t^2`. + static bool IsAnImaginarySum(Entity slope, Entity intercept) + { + var ratio = Functions.PartialFractions.Bare((slope / intercept).InnerSimplified); + if (ratio.Evaled is not Number.Complex) + ratio = Functions.PartialFractions.Bare(ratio.Simplify()); + return ratio.Evaled == MathS.i.Evaled || ratio.Evaled == (-MathS.i).Evaled; + } + // The integrand read as a constant, roots of linears, whole powers of polynomials above the + // bar and of linears below it. + Entity constant = Number.Integer.One; + var above = new List(); + var below = new List(); + var degreeBelow = 0; + var imaginarySumBelow = false; + var roots = new List<(Entity Linear, Entity Slope, Entity Intercept, Number.Rational Power)>(); + bool Read(Entity node, EInteger times) + { + switch (node) + { + case Mulf(var left, var right): + return Read(left, times) && Read(right, times); + case Divf(var dividend, var divisor): + return Read(dividend, times) && Read(divisor, times.Negate()); + case Powf(var @base, Number.Integer whole) when @base.ContainsNode(t): + return Read(@base, times.Multiply(whole.EInteger)); + case Powf(var @base, Number.Rational power) when @base.ContainsNode(t): + { + var exponent = power.ERational.Multiply(ERational.FromEInteger(times)); + if (exponent.Denominator.Equals(EInteger.One)) + return Read(@base, exponent.Numerator); + if (!TreeAnalyzer.TryGetPolyLinear(@base, t, out var slope, out var intercept) + || slope.ContainsNode(t) || intercept.ContainsNode(t) + || VanishesIdentically(slope) || VanishesIdentically(intercept) || IsAnImaginarySum(slope, intercept)) + return false; + roots.Add((@base, slope, intercept, Number.Rational.Create(exponent))); + return true; + } + case var free when !free.ContainsNode(t): + constant *= MathS.Pow(free, Number.Integer.Create(times)); + return true; + default: + if (!TreeAnalyzer.TryGetPolynomial(node, t, out var monomials) + || monomials.Values.Any(coefficient => coefficient.ContainsNode(t)) || monomials.Keys.Any(degree => degree.Sign < 0) + || !times.CanFitInInt32()) + return false; + if (times.Sign > 0) + { + above.Add(times.Equals(EInteger.One) ? node : MathS.Pow(node, Number.Integer.Create(times))); + return true; + } + if (!TreeAnalyzer.TryGetPolyLinear(node, t, out var linearSlope, out var linearIntercept) + || linearSlope.ContainsNode(t) || linearIntercept.ContainsNode(t) || VanishesIdentically(linearSlope)) + return false; + var multiplicity = times.Negate(); + // `(A + i A t)^k` is `A^k (1 + i t)^k`, a whole power: with the constant out + // in front the split is over numbers, and its terms' answers a few lines each, + // where with `A` in the linear they came to ten times the length. + if (!VanishesIdentically(linearIntercept) && IsAnImaginarySum(linearSlope, linearIntercept)) + { + imaginarySumBelow = true; + constant /= MathS.Pow(linearIntercept, Number.Integer.Create(multiplicity)); + node = (1 + Functions.PartialFractions.Bare((linearSlope / linearIntercept).Simplify()) * t).InnerSimplified; + } + below.Add(multiplicity.Equals(EInteger.One) ? node : MathS.Pow(node, Number.Integer.Create(multiplicity))); + degreeBelow += multiplicity.ToInt32Checked(); + return true; + } + } + if (!Read(inT, EInteger.One) || degreeBelow > 12) + return null; + // Two roots beside a polynomial or a linear below, or one root below the bar beside a power + // of the imaginary sum below it: two roots alone over `1 + t^2`, and a root above the bar, + // the root's substitution answers as soon, and in the same few terms. + if (roots.Count == 2 + ? above.Count == 0 && below.Count == 0 + || VanishesIdentically(roots[0].Slope * roots[1].Intercept - roots[1].Slope * roots[0].Intercept) + : roots.Count != 1 || !imaginarySumBelow || roots[0].Power.ERational.Sign > 0) + return null; + + // Divided by `1 + t^2` and the linears as written, and the proper rest split over the + // linears with `1 + t^2` written as its own two. + static Entity Product(IEnumerable factors) => + factors.Aggregate((Entity?)null, (all, factor) => all is null ? factor : all * factor) ?? Number.Integer.One; + var numerator = Product(above); + var written = Product(below.Append(1 + MathS.Sqr(t))); + var overTheLinears = Product(below.Append(1 + MathS.i * t).Append(1 - MathS.i * t)); + if (OverEachLinear(numerator, written, overTheLinears, degreeBelow + 2, t) is not { } terms) + return null; + var ofTheRoots = Product(roots.Select(root => MathS.Pow(root.Linear, root.Power))); + Entity total = Number.Integer.Zero; + foreach (var term in terms) + { + if (term.Evaled is Number.Complex { IsZero: true }) + continue; + if (Integration.ComputeAsAQuestionOfItsOwn((term * ofTheRoots).InnerSimplified, t, integrateByParts) is not { } integrated + || integrated.Nodes.Any(node => node == MathS.NaN)) + return null; + total += integrated; + } + // Checked by its derivative, and where the intervals cannot tell -- the arctangent of a + // complex argument that the answer to `(c + d t)^(5/2)/((a + b t)^(5/2) (1 + i t))` holds, + // right to ninety digits at every point -- beside two roots refused only where it + // differs at a point where the integrand, then real, is. + var integrandInT = numerator / written * ofTheRoots; + if (!Functions.PartialFractions.DerivativeHoldsAtSampledPoints(total, integrandInT, t) + && (roots.Count == 1 || Functions.PartialFractions.DerivativeDiffersWhereReal(total, integrandInT, t))) + return null; + return (constant / rate * total).Substitute(t, MathS.Tan(argument)); + } + /// /// A rational function of tan(z) beside a power of S = a + c tan(z) with /// c = ±i a, below the bar or not whole, integrated in S: tan(z) is diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index d55feb5a3..6f987d3d7 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -817,6 +817,10 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // And an exponential of a hyperbolic sine or cosine of a linear beside a function of it // over its derivative, under u = twice that sine or cosine, which arrive as exponentials. if ((answer = IndefiniteIntegralSolver.SolveAnExponentialOfAHyperbolicFunctionBesideItsDerivative(expr, x, integrateByParts)) is { }) return answer; + // A rational function of the tangent beside two roots of linears in it, or one beside a + // whole power of a + i a tan(z) below the bar, over the linears of 1 + tan^2: in the root + // of the linears' quotient 1 + tan^2 is a symbolic quartic, and a search past the budget. + if ((answer = IndefiniteIntegralSolver.SolveARationalFunctionOfTheTangentBesideRootsOfLinearsInIt(expr, x, integrateByParts)) is { }) return answer; // `A + i A tan(z)` is `A e^(i z)/cos(z)`, which beside a polynomial is a shape the // closed rules answer, where the imaginary unit in the coefficient is read by none. // The same identity beside a power of the secant, where the powers are not whole and add diff --git a/Sources/Tests/UnitTests/Calculus/ARationalFunctionOfTheTangentBesideRootsOfLinearsInItIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ARationalFunctionOfTheTangentBesideRootsOfLinearsInItIntegralTest.cs new file mode 100644 index 000000000..dc33ba2e7 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ARationalFunctionOfTheTangentBesideRootsOfLinearsInItIntegralTest.cs @@ -0,0 +1,59 @@ +// +// 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 beside two roots of linears in it, or one root below the + /// bar beside a whole power of a + i a tan, integrated in t = tan(g + h x) over the + /// partial fractions of its rational part with 1 + t^2 written as (1 + i t)(1 - i t). + /// In the root of the linears' quotient 1 + t^2 is a symbolic quartic, and every one of + /// these ran past the budget. Rubi's 4.3.4.2 and 4.3.2.1. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class ARationalFunctionOfTheTangentBesideRootsOfLinearsInItIntegralTest + { + [Theory] + [InlineData("(c + d*tan(g + h*x))^(5/2)*(A + B*tan(g + h*x) + C*tan(g + h*x)^2)/sqrt(a + b*tan(g + h*x))")] + [InlineData("(c + d*tan(g + h*x))^(5/2)*(A + B*tan(g + h*x) + C*tan(g + h*x)^2)/(a + b*tan(g + h*x))^(3/2)")] + [InlineData("(a + b*tan(g + h*x))^(5/2)*(A + B*tan(g + h*x) + C*tan(g + h*x)^2)/(c + d*tan(g + h*x))^(5/2)")] + [InlineData("(A + B*tan(g + h*x) + C*tan(g + h*x)^2)/((a + b*tan(g + h*x))^(3/2)*(c + d*tan(g + h*x))^(3/2))")] + [InlineData("(A + B*tan(g + h*x) + C*tan(g + h*x)^2)/((a + b*tan(g + h*x))^(3/2)*(c + d*tan(g + h*x))^(5/2))")] + [InlineData("1/((a + i*a*tan(g + h*x))^3*(c + d*tan(g + h*x))^(3/2))")] + [InlineData("1/((a + i*a*tan(g + h*x))^2*(c + d*tan(g + h*x))^(5/2))")] + [InlineData("1/((a + i*a*tan(g + h*x))^3*(c + d*tan(g + h*x))^(5/2))")] + public void OverTheLinearsOfOnePlusTheTangentSquared(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 30000, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 2.1).Substitute("d", 0.4) + .Substitute("A", 0.9).Substitute("B", 1.7).Substitute("C", 0.6).Substitute("g", 0.2).Substitute("h", 1.1); + var derivative = Pinned(integral).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -0.9, -0.5, 0.1, 0.4, 0.7, 1.0 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + if (want.IsNaN) + continue; + compared++; + 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}"); + } + Assert.True(compared >= 5, $"only {compared} points could be compared for {integrand}"); + } + } +}