diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 36fbccf52..5d77674d5 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -543,6 +543,24 @@ that `sec(x)^2/(a + b sin(x))` is rational in them rather than declined at once | `"tan(x)^4/(a + a*cos(x))".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative in `tan(x/2)` | | `"csc(x)^2/(a + a*cos(x))".ToEntity().Integrate("x")` | `integral(...)` | `(tan(x/2)^3/12 + tan(x/2)/2 - 1/(4 tan(x/2)))/a` | +### A power of `a + i a tan` beside a power of the secant is integrated as an exponential + +**Answers where there were none.** `sqrt(a + i a tan(x))/sqrt(c sec(x))` is `sqrt(a/c) e^(i x/2)` +wherever the cosine is positive, and was declined, with every other pair of such powers, one of them +not whole, whose exponents add up to a whole number: Rubi's 4.3.1.2 has 43 of them. `a + i a tan(z)` is +`a sec(z) e^(i z)` on the real line, so the integrand is a constant on every interval where it is +continuous times a power of the secant and an exponential, which in `w = e^(i z)` is rational in a root +of `w`. Three of the 43, `(c sec(x))^p/(a + i a tan(x))^p` for `p` a half, three halves and five, were +answered on the unreleased master through the power of a product taken apart, wrong by a constant +wherever the cosine is negative; they are answered with the rest now +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sqrt(a + i*a*tan(x))/sqrt(c*sec(x))".ToEntity().Integrate("x")` | `integral(...)` | the integrand times `2/i`, through `e^(i x)` | +| `"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 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 2f20ae968..d7b12536b 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -23594,6 +23594,136 @@ static bool IsAWholeNumberOfSteps(ERational difference) return Integration.ComputeAsTheSameQuestion(inExponentials.InnerSimplified, x, integrateByParts); } + /// + /// A power of A + i A tan(z) beside a power of the secant of the same argument, one + /// of them not whole and the two adding up to a whole number k, integrated as the + /// exponential it is: A + i A tan(z) is A sec(z) e^(i z) on the real line, so + /// (A + i A tan(z))^n (c sec(z))^m is a constant on every interval where it is + /// continuous times sec(z)^k e^(i n z), which in w = e^(i z) is + /// (2 w/(w^2 + 1))^k w^n, and dz is dw/(i w). + /// + /// + /// + /// sqrt(a + i a tan(x))/sqrt(c sec(x)) is sqrt(a/c) e^(i x/2) wherever the + /// cosine is positive, and was declined, with every other power of the pair whose exponents + /// add up to a whole number: a root of a sum with the imaginary unit in it beside a root of the + /// secant is read by no rule, and e^(i x/2)/cos(x), what the positive sums leave, is + /// declined as well. Rubi's 4.3.1.2. + /// + /// + /// The constant is not written: the answer is the integrand times the antiderivative in + /// w over what that antiderivative differentiates back to, sec(z)^k (e^(i z))^n + /// times i and the slope, a quotient whose logarithmic derivative is zero -- both + /// halves have n (tan(z) + i) z' -- so it is constant wherever it is continuous, and + /// the answer holds on every interval where the integrand is. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + /// + internal static Entity? SolveAPowerOfAnImaginaryTangentBesideAPowerOfTheSecant(Entity expr, Entity.Variable x, bool integrateByParts) + { + if (!expr.Nodes.Any(node => node is Secantf) || !expr.Nodes.Any(node => node is Tanf)) + return null; + Entity? argument = null; + var plus = false; + Number.Rational? tangentPower = null, secantPower = null; + Entity constant = Number.Integer.One; + Entity varying = Number.Integer.One; + foreach (var (factor, underneath) in FactorsOfTheIntegrand(expr)) + { + if (!factor.ContainsNode(x)) + { + constant = underneath ? constant / factor : constant * factor; + continue; + } + varying = underneath ? varying / factor : varying * factor; + var (@base, power) = factor is Powf(var b, var p) && p.Evaled is Number.Rational r + ? (b, r) + : (factor, (Number.Rational)Number.Integer.One); + if (underneath) + power = (Number.Rational)(-power); + if (TryReadAnImaginaryTangent(@base, x, out var tangentOf, out var isPlus)) + { + if (tangentPower is not null || argument is not null && argument != tangentOf) + return null; + (tangentPower, argument, plus) = (power, tangentOf, isPlus); + continue; + } + var secant = @base switch + { + Secantf => @base, + Mulf(var left, Secantf right) when !left.ContainsNode(x) => right, + Mulf(Secantf left, var right) when !right.ContainsNode(x) => left, + _ => null + }; + if (secant is not Secantf(var secantOf) || secantPower is not null || argument is not null && argument != secantOf) + return null; + (secantPower, argument) = (power, secantOf); + } + if (tangentPower is null || secantPower is null || argument is null + || tangentPower is Number.Integer && secantPower is Number.Integer + || (tangentPower + secantPower) is not Number.Integer { EInteger: var whole } || !whole.CanFitInInt32() + || !TreeAnalyzer.TryGetPolyLinear(argument, x, out var slope, out _) || TreeAnalyzer.IsZero(slope)) + return null; + var k = whole.ToInt32Checked(); + var phase = plus ? tangentPower : (Number.Rational)(-tangentPower); + // In w = e^(i z): sec(z)^k e^(i n z) dz is (2 w)^k (w^2 + 1)^(-k) w^(n - 1) dw / i. + var w = Variable.CreateUnique(expr, "w_exp"); + var inW = k >= 0 + ? MathS.Pow(2, k) * MathS.Pow(w, (phase + k - 1).InnerSimplified) / MathS.Pow(MathS.Sqr(w) + 1, k) + : MathS.Pow(2, k) * MathS.Pow(MathS.Sqr(w) + 1, -k) * MathS.Pow(w, (phase + k - 1).InnerSimplified); + if (Integration.ComputeAsAQuestionOfItsOwn(inW.InnerSimplified, w, integrateByParts) is not { } inWAnswer + || inWAnswer.Nodes.Any(node => node == MathS.NaN)) + return null; + var exponential = MathS.Pow(MathS.e, MathS.i * argument); + var differentiatesBackTo = MathS.Pow(MathS.Sec(argument), k) * MathS.Pow(exponential, phase); + return constant * varying * inWAnswer.Substitute(w, exponential) / (MathS.i * slope * differentiatesBackTo); + } + + /// + /// A + i A tan(z) or A - i A tan(z), with a constant A: the argument, + /// and whether the imaginary unit comes with a plus. + /// + private static bool TryReadAnImaginaryTangent(Entity sum, Entity.Variable x, out Entity argument, out bool plus) + { + (argument, plus) = (Number.Integer.Zero, false); + if (sum is not Sumf and not Minusf || !sum.ContainsNode(x)) + return false; + Entity free = Number.Integer.Zero; + Entity? coefficient = null; + Entity? tangentOf = null; + foreach (var term in Sumf.LinearChildren(sum)) + { + if (!term.ContainsNode(x)) + { + free += term; + continue; + } + if (coefficient is not null) + return false; + Entity factors = Number.Integer.One; + foreach (var factor in Mulf.LinearChildren(term)) + if (factor is Tanf(var inner) && tangentOf is null) + tangentOf = inner; + else if (factor.ContainsNode(x)) + return false; + else + factors *= factor; + if (tangentOf is null) + return false; + coefficient = factors; + } + if (coefficient is null || tangentOf is null || free == Number.Integer.Zero) + return false; + var ratio = Functions.PartialFractions.Bare((coefficient / free).InnerSimplified); + if (ratio.Evaled is not Number.Complex) + ratio = Functions.PartialFractions.Bare(ratio.Simplify()); + plus = ratio.Evaled == MathS.i.Evaled; + if (!plus && ratio.Evaled != (-MathS.i).Evaled) + return false; + argument = tangentOf; + return true; + } + /// /// A cos(y) + i A sin(y) below the bar, written as the exponential it is: /// A e^(i y), and A cos(y) - i A sin(y) as A e^(-i y). Beside a power diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 0d7d9f42d..2f968045c 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -669,6 +669,12 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => if ((answer = IndefiniteIntegralSolver.SolveAnExponentialOfAHyperbolicFunctionBesideItsDerivative(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 + // up to a whole number: an exponential over a power of the cosine, in e^(i z). Before the + // rule below, which writes a power of the identity that is not whole as the power of a + // 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; 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/ImaginaryTangentBesideTheSecantIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ImaginaryTangentBesideTheSecantIntegralTest.cs new file mode 100644 index 000000000..f29f5865a --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ImaginaryTangentBesideTheSecantIntegralTest.cs @@ -0,0 +1,55 @@ +// +// 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 power of a + i a tan(x) beside a power of the secant, one of them not whole and + /// the two adding up to a whole number: a + i a tan(x) is a sec(x) e^(i x) on + /// the real line, so the integrand is a constant on every interval where it is continuous times + /// a power of the secant and an exponential. Rubi's 4.3.1.2. The integrands are complex along + /// the real line and are compared as complex numbers, where the cosine is negative as well, + /// which is where the fourth row's answer on master was off by a constant factor. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class ImaginaryTangentBesideTheSecantIntegralTest + { + [Theory] + [InlineData("sqrt(a + i*a*tan(x))/sqrt(k*sec(x))")] + [InlineData("sqrt(k*sec(x))*sqrt(a + i*a*tan(x))")] + [InlineData("(a + i*a*tan(x))^(3/2)/(k*sec(x))^(7/2)")] + [InlineData("(k*sec(x))^(5/2)/(a + i*a*tan(x))^(5/2)")] + [InlineData("(k*sec(c + d*x))^(3/2)*(a - i*a*tan(c + d*x))^(3/2)")] + public void AsTheExponentialItIs(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Assert.DoesNotContain("NaN", integral.Stringize()); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("k", 0.8).Substitute("c", 0.4).Substitute("d", 1.1); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -2.6, -1.1, -0.4, 0.3, 0.8, 1.2, 2.0 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + if (want.IsNaN) + continue; + compared++; + 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}"); + } + Assert.True(compared >= 6, $"only {compared} points could be compared for {integrand}"); + } + } +}