diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index b962bfd10..963779013 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -793,6 +793,23 @@ wherever the cosine is negative; they are answered with the rest now | `"sqrt(c*sec(x))*sqrt(a + i*a*tan(x))".ToEntity().Integrate("x")` | `integral(...)` | the integrand times an antiderivative in `e^(i x/2)` over its derivative | | `"(c*sec(x))^(5/2)/(a + i*a*tan(x))^(5/2)".ToEntity().Integrate("x")` | `integral(...)` | the integrand times a power of `e^(i x)` | +### 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 +Rubi's 4.3.3.1 with a whole power of the tangent or cotangent beside `A + B tan` over a power of +`a + i a tan` ran past the budget, whole powers and half-odd ones alike. With `S = a ± i a tan(z)`, +`tan(z)` is `(S - a)/(± i a)` and `dz = c dS/(S (S - 2a))`, so each is a rational function of `S` +beside a power of it, whose factors `S`, `S - 2a` and `S - a` have no imaginary root. Beside a +second such sum, `q - i q tan(z)`, which is linear in `S`, the sum under a power that is not whole is +the variable: `(a + i a tan(z))/(q - i q tan(z))^(3/2)`, from 4.3.2.1, ran past the budget as well +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"tan(c + d*x)^2*(k + q*tan(c + d*x))/sqrt(a + i*a*tan(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | powers of `sqrt(a + i a tan(c + d x))` and, piecewise in the sign of `a`, an arctangent or a logarithm of it | +| `"cot(c + d*x)^2*(k + q*tan(c + d*x))/(a + i*a*tan(c + d*x))^4".ToEntity().Integrate("x")` | `integral(...)` | powers and logarithms of `a + i a tan(c + d x)`, of `a - i a tan(c + d x)` and of `tan(c + d x)` | +| `"(a + i*a*tan(c + d*x))/(q - i*q*tan(c + d*x))^(3/2)".ToEntity().Integrate("x")` | `integral(...)` | `-2 i a (q - i q tan(c + d x))^(-3/2)/(3d)`, written longer | + ### A rational function with complex coefficients is integrated through its real and imaginary parts **Answers where there were none.** `1/((1 + i x)^2 (1 + x^2))` was declined: the rational diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 10e0310fe..5bc58433b 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -25289,6 +25289,145 @@ static bool IsAWholeNumberOfSteps(ERational difference) return Integration.ComputeAsAQuestionOfItsOwn(expanded.Expand().InnerSimplified, x, integrateByParts); } + /// + /// 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 + /// (S - a)/c, and since c^2 = -a^2, dS/dz = c sec(z)^2 = S (S - 2a)/c, so + /// dz = c dS/(S (S - 2a)). + /// + /// + /// + /// Rubi's cot(c + d x)^3 (A + B tan(c + d x))/(a + i a tan(c + d x))^2 and the rest of + /// its 4.3.3.1 with a power of the tangent beside (A + B tan) over a power of + /// a + i a tan ran past the budget, whole powers and half-odd ones alike: under + /// u = tan(z) each is a rational function over 1 + i u and 1 - i u with + /// symbols in every coefficient. In S the factors are S, S - 2a and, + /// from a cotangent, S - a, with no imaginary root among them, and the same + /// integrands are a second or two; a half-odd power of S is a root of a linear. + /// + /// + /// Exact wherever the integrand is defined: the substitution is the identity + /// sec(z)^2 = 1 + tan(z)^2 read in S, and the power of S is the + /// integrand's own. Only whole powers of the tangent and cotangent of z beside it, + /// and nothing else in x; a base standing only to positive whole powers is a polynomial in + /// the tangent, which the rules for those answer. + /// + /// + /// Beside a second such sum of the same argument, q - i q tan(z), which is linear in + /// the first, the one under a power that is not whole is the variable and the other a whole + /// power of a linear in it: (a + i a tan(z))/(q - i q tan(z))^(3/2), Rubi's 4.3.2.1, + /// ran past the budget too. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + /// + internal static Entity? SolveInTheImaginarySumOfAConstantAndATangent(Entity expr, Entity.Variable x, bool integrateByParts) + { + var sums = new List<(Entity Base, Entity Constant, Entity Coefficient, Entity Argument)>(); + foreach (var node in expr.Nodes) + { + if (node is not (Sumf or Minusf) || !node.ContainsNode(x) || sums.Any(found => found.Base == node)) + continue; + Entity sum = Number.Integer.Zero; + Entity? factor = null, inner = null; + var read = true; + foreach (var term in Sumf.LinearChildren(node)) + { + if (!term.ContainsNode(x)) + { + sum = sum == Number.Integer.Zero ? term : sum + term; + continue; + } + if (factor is not null) + { + read = false; + break; + } + Entity product = Number.Integer.One; + foreach (var piece in Mulf.LinearChildren(term)) + { + if (piece is Tanf(var y) && inner is null) + inner = y; + else if (!piece.ContainsNode(x)) + product = product == Number.Integer.One ? piece : product * piece; + else + { + read = false; + break; + } + } + if (!read || inner is null) + { + read = false; + break; + } + factor = product; + } + if (!read || factor is null || inner is null || sum == Number.Integer.Zero) + continue; + var ratio = Functions.PartialFractions.Bare((factor / sum).InnerSimplified); + if (ratio.Evaled is not Number.Complex) + ratio = Functions.PartialFractions.Bare(ratio.Simplify()); + if (ratio.Evaled != MathS.i.Evaled && ratio.Evaled != (-MathS.i).Evaled) + continue; + sums.Add((node, sum, factor, inner)); + } + // Two such sums, `a + i a tan(z)` and `c - i c tan(z)`, are each linear in the other: in + // the one under a power that is not whole the other is a whole power of a linear, where + // in the other it would be the root of one. Where both or neither are, declined. + bool NotWhole(Entity sum) => expr.Nodes.Any(node => node is Powf(var b, Number.Rational p) && b == sum && p is not Number.Integer); + var chosen = sums.Count switch + { + 1 => sums[0], + 2 when NotWhole(sums[0].Base) != NotWhole(sums[1].Base) => NotWhole(sums[0].Base) ? sums[0] : sums[1], + _ => default, + }; + var (@base, constant, coefficient, argument) = chosen; + if (@base is null || constant is null || coefficient is null || argument is null + || !TreeAnalyzer.TryGetPolyLinear(argument, x, out var rate, out _) || rate.ContainsNode(x) + || rate.Evaled is Number.Complex { IsZero: true }) + return null; + // Below the bar, or to a power that is not whole: a positive whole power alone is a + // polynomial in the tangent. + var below = Functions.SingleQuotient.Of(Functions.SingleQuotient.Combine(expr)).Denominator; + if (!below.Nodes.Contains(@base) + && !expr.Nodes.Any(node => node is Powf(var b, Number.Rational p) && b == @base && p is not Number.Integer)) + return null; + var s = Variable.CreateUnique(expr, "s_imaginary_tangent"); + var tangent = (s - constant) / coefficient; + // The other sum, `q + c' tan(z)`, written as the linear in `S` it is, `(c'/c) (S - r)` + // for `r = a - q c/c'`, its root spelled `2 a` where it is that: there it cancels the + // `S - 2a` of `dz` as written, where `q + c' (S - a)/c` cancelled nothing, and the pole + // it left made `(a + i a tan(z))^2/sqrt(q - i q tan(z))` fourteen thousand characters. + Entity? other = null, otherInS = null; + if (sums.Count == 2) + { + var (otherBase, otherConstant, otherCoefficient, _) = sums[0].Base == @base ? sums[1] : sums[0]; + var ratio = Functions.PartialFractions.InLowestTermsOverTheSymbols(otherCoefficient / coefficient); + var root = Functions.PartialFractions.InLowestTermsOverTheSymbols(constant - otherConstant * coefficient / otherCoefficient); + if (Functions.PartialFractions.IsZeroAsAValue(root - 2 * constant)) + root = 2 * constant; + (other, otherInS) = (otherBase, ratio * (s - root)); + } + var rewritten = expr.Replace(node => node == @base ? s : other is not null && node == other ? otherInS! : node).Replace(node => node switch + { + Tanf(var y) when y == argument => tangent, + Cotanf(var y) when y == argument => 1 / tangent, + _ => node, + }); + if (rewritten.ContainsNode(x)) + return null; + // No power that is not whole but of S itself: a root of the tangent is a root of + // `(S - a)/c` with `c` imaginary, and taken apart over the constant it changed its + // branch -- `cot(z)^(3/2) (A + B tan(z))/(a + i a tan(z))^2` and `tan(z)^(8/3)/(a + i a tan(z))` + // came back wrong at every point. + if (rewritten.Nodes.Any(node => node is Powf(var radicand, var power) && power is not Number.Integer && radicand != s)) + return null; + var inS = (rewritten * coefficient / (s * (s - 2 * constant)) / rate).InnerSimplified; + if (Integration.ComputeAsAQuestionOfItsOwn(inS, s, integrateByParts) is not { } inTermsOfS) + return null; + return inTermsOfS.Substitute(s, @base); + } + /// /// A + i A tan(z) is A e^(i z)/cos(z), and A + i A cot(z) is /// i A e^(-i z)/sin(z) -- exactly, wherever the tangent is defined, since diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 61702adbe..b7cf7cf7c 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -675,6 +675,8 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => // product and takes it apart, and so answers (c sec)^(5/2)/(a + i a tan)^(5/2) with the // wrong constant wherever the cosine is negative. if ((answer = IndefiniteIntegralSolver.SolveAPowerOfAnImaginaryTangentBesideAPowerOfTheSecant(expr, x, integrateByParts)) is { }) return answer; + // A rational function of the tangent beside a power of a + i a tan(z), in that sum. + if ((answer = IndefiniteIntegralSolver.SolveInTheImaginarySumOfAConstantAndATangent(expr, x, integrateByParts)) is { }) return answer; if ((answer = IndefiniteIntegralSolver.SolveByWritingAnImaginaryTangentAsAnExponential(expr, x, integrateByParts)) is { }) return answer; // And `A cos(z) + i A sin(z)`, which is `A e^(i z)`, where no rotation is real. if ((answer = IndefiniteIntegralSolver.SolveByWritingAnImaginarySumOfACosineAndASineAsAnExponential(expr, x, integrateByParts)) is { }) return answer; diff --git a/Sources/Tests/UnitTests/Calculus/RationalInTheTangentBesideAnImaginarySumIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/RationalInTheTangentBesideAnImaginarySumIntegralTest.cs new file mode 100644 index 000000000..32b0ad7ba --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/RationalInTheTangentBesideAnImaginarySumIntegralTest.cs @@ -0,0 +1,52 @@ +// +// 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 tan(z) beside a power of S = a ± i a tan(z), integrated + /// in S, where dz = c dS/(S (S - 2a)): the factors are S, S - 2a and + /// S - a, with no imaginary root among them. Beside q - i q tan(z), a linear in + /// S, the sum under a power that is not whole is the variable. Rubi's 4.3.3.1 and 4.3.2.1. + /// #718 + /// + /// Compared as complex numbers, the integrand being complex. + [Trait("Area", "Calculus")] + public sealed class RationalInTheTangentBesideAnImaginarySumIntegralTest + { + [Theory] + [InlineData("cot(c + d*x)^3*(k + q*tan(c + d*x))/(a + i*a*tan(c + d*x))")] + [InlineData("cot(c + d*x)*(k + q*tan(c + d*x))/(a + i*a*tan(c + d*x))^2")] + [InlineData("tan(c + d*x)^2*(k + q*tan(c + d*x))/sqrt(a + i*a*tan(c + d*x))")] + [InlineData("(k + q*tan(c + d*x))/(a - i*a*tan(c + d*x))^(3/2)")] + [InlineData("cot(c + d*x)^2*(k + q*tan(c + d*x))/(a + i*a*tan(c + d*x))^4")] + [InlineData("(a + i*a*tan(c + d*x))/(q - i*q*tan(c + d*x))^(3/2)")] + [InlineData("sqrt(q - i*q*tan(c + d*x))/(a + i*a*tan(c + d*x))")] + [InlineData("(a + i*a*tan(c + d*x))^2/sqrt(q - i*q*tan(c + d*x))")] + public void IsIntegratedInTheSum(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("c", 0.4).Substitute("d", 1.1) + .Substitute("k", 1.1).Substitute("q", 0.6); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in new[] { -1.1, -0.6, 0.3, 0.9 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + 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}"); + } + } + } +}