diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index f82ee966a..c8ea6e633 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -527,6 +527,20 @@ interval without a case for each. Only where nothing else answers the integrand | `"acoth(x)^2/(1-x^2)^2".ToEntity().Integrate("x")` | `integral(...)` | 1,304 characters, with `(ln((x + 1)/(x - 1)) - ln((1 + x)/(1 - x)))/2` for `K` | | `"acoth(c*x)^2/(1-c^2*x^2)^2".ToEntity().Integrate("x")` | `integral(...)` | 1,681 characters | +### A power of a sum of exponentials that is one exponential is written as it + +**Answers where there were none, and a shorter one.** The parser writes `cosh(u) + sinh(u)` as +`(e^u + e^(-u))/2 + (e^u - e^(-u))/2`, and nothing collected it. Rubi's 6.7.1 `F^(a + b x) (cosh(c + d x) + sinh(c + d x))^n` +and its twin with the difference were declined. A power of such a sum is now written as the one exponential it is, +`c^n e^(p n u)` for a sum that is `c e^(p u)`, which holds for a real `u` since `e^(p u)` is positive +([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"F^(a+b*x)*(cosh(c+d*x)+sinh(c+d*x))^n".ToEntity().Integrate("x")` | `integral(...)` | 160 characters | +| `"F^(a+b*x)*(cosh(c+d*x)-sinh(c+d*x))^n".ToEntity().Integrate("x")` | `integral(...)` | 165 characters | +| `"(cosh(x)+sinh(x))^3".ToEntity().Integrate("x")` | 787 characters | `e^(3 x)/3`, 19 characters | + ### A function of the hyperbolic tangent alone is integrated under `u = tanh` **Answers where there were none.** `tanh(x)/sqrt(a + b tanh(x)^2 + c tanh(x)^4)` was declined, and so were 35 more of diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 37e3c6c9d..beb4e0234 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -41968,6 +41968,60 @@ Entity Times(Entity term) /// Set while asks its question. [System.ThreadStatic] private static bool clearingANegativeExponential; + /// + /// A power of a sum of exponentials of x that is one exponential, written as it: Rubi's + /// F^(a + b x) (cosh(c + d x) + sinh(c + d x))^n is F^(a + b x) e^(n (c + d x)). + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + /// + /// The parser writes the hyperbolic functions as exponentials, and the sum is + /// (e^u + e^(-u))/2 + (e^u - e^(-u))/2 as written, which nothing collects: beside + /// F^(a + b x) it was declined. With each exponential a whole power of the first, + /// w, the base is a Laurent polynomial in w, and where one coefficient is left it + /// is c w^p; (c w^p)^n is c^n w^(p n) for a positive w, which e to a + /// real power is. At the question asked. + /// + internal static Entity? SolveByFoldingASumOfExponentialsIntoOne(Entity expr, Entity.Variable x, bool integrateByParts) + { + if (!Integration.AnsweringTheQuestionAsked) + return null; + static bool IsAnExponential(Entity node, Entity.Variable x) => node is Powf(var @base, var exponent) && @base == MathS.e && exponent.ContainsNode(x); + var folded = expr.Replace(node => + { + if (node is not Powf(Sumf or Minusf, var power) raised || power.ContainsNode(x) || power == Number.Integer.One) + return node; + var sum = raised.Base; + var exponentials = sum.Nodes.Where(inner => IsAnExponential(inner, x)).Distinct().ToList(); + if (exponentials.Count < 2 + || sum.Replace(inner => IsAnExponential(inner, x) ? Number.Integer.One : inner).ContainsNode(x)) + return node; + // Each a whole power of the first, e^(m + k x). + var first = ((Powf)exponentials[0]).Exponent; + var powers = new Dictionary(); + foreach (var exponential in exponentials) + if (Functions.PartialFractions.Bare((((Powf)exponential).Exponent / first).Simplify()) is Number.Integer { EInteger: var whole } && whole.CanFitInInt32()) + powers[exponential] = whole.ToInt32Unchecked(); + else + return node; + var w = Variable.CreateUnique(expr, "w_fold"); + var inW = sum.Replace(inner => powers.TryGetValue(inner, out var k) ? MathS.Pow(w, Number.Integer.Create(k)) : inner); + // Expanded, `(w + 1/w)/2 + (w - 1/w)/2` is `w provided not w = 0`, and w is never 0. + if (!TreeAnalyzer.TryGetPolynomial(Functions.PartialFractions.Bare(inW.Expand()), w, out var read)) + return node; + var left = read.Where(pair => !Functions.PartialFractions.IsZeroAsAValue(pair.Value)).ToList(); + if (left.Count != 1 || !left[0].Key.CanFitInInt32()) + return node; + var (degree, coefficient) = (left[0].Key.ToInt32Unchecked(), Functions.PartialFractions.Bare(left[0].Value.InnerSimplified)); + var exponentialOfThePower = MathS.Pow(MathS.e, (power * degree * first).InnerSimplified); + return coefficient == Number.Integer.One ? exponentialOfThePower : MathS.Pow(coefficient, power) * exponentialOfThePower; + }); + if (folded == expr) + return null; + // Inner-simplified, which takes the `ln(e)` the rule for `F^u` writes for the base to 1. + var answer = Integration.ComputeAsTheSameQuestion(folded.InnerSimplified, x, integrateByParts)?.InnerSimplified; + return answer is not null && Functions.PartialFractions.DerivativeHoldsAtSampledPoints(answer, expr, x) ? answer : null; + } + /// /// A squared sine and a squared cosine of one argument in a sum, written as the one they /// add up to: Rubi's e^(cos(x)^2 + sin(x)^2) is e x. diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 110f98822..803834f4e 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -695,6 +695,8 @@ internal static bool ASumOverRootsWouldAnswer // And a squared sine beside the squared cosine of its argument, the one they add up to: // `e^(cos(x)^2 + sin(x)^2)` was declined, written with an x its value does not hold. if ((answer = IndefiniteIntegralSolver.SolveByFoldingAPythagoreanPair(expr, x, integrateByParts)) is { }) return answer; + // And a power of a sum of exponentials that is one exponential, the hyperbolic `cosh(u) + sinh(u)`. + if ((answer = IndefiniteIntegralSolver.SolveByFoldingASumOfExponentialsIntoOne(expr, x, integrateByParts)) is { }) return answer; // An arctangent over a power of d (1 +- i c x), under x = tan(u)/c, where 1 + i tan(u) is // e^(i u)/cos(u): the rules below ran out of time on `(a + b atan(c x))^2/(d + i c d x)^2`. if ((answer = IndefiniteIntegralSolver.SolveAnArctangentOverAPowerOfOnePlusICXUnderTheTangent(expr, x, integrateByParts)) is { }) return answer; diff --git a/Sources/Tests/UnitTests/Calculus/APowerOfASumOfExponentialsThatIsOneIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/APowerOfASumOfExponentialsThatIsOneIntegralTest.cs new file mode 100644 index 000000000..a7f34ec90 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/APowerOfASumOfExponentialsThatIsOneIntegralTest.cs @@ -0,0 +1,49 @@ +// +// 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 sum of exponentials that is one exponential, written as it: the parser writes + /// cosh(u) + sinh(u) as (e^u + e^(-u))/2 + (e^u - e^(-u))/2, which nothing collected, and + /// Rubi's 6.7.1 F^(a + b x) (cosh(c + d x) + sinh(c + d x))^n was declined. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class APowerOfASumOfExponentialsThatIsOneIntegralTest + { + [Theory] + [InlineData("F^(a + b*x)*(cosh(c + d*x) + sinh(c + d*x))^n", 400)] + [InlineData("F^(a + b*x)*(cosh(c + d*x) - sinh(c + d*x))^n", 400)] + [InlineData("(cosh(x) + sinh(x))^3", 40)] + [InlineData("x*(cosh(x) - sinh(x))^(5/2)", 120)] + public void IsOneExponential(string integrand, int characters) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < characters, $"the antiderivative of {integrand} is {text.Length} characters: {text}"); + Entity Pinned(Entity e) => e.Substitute("F", 1.7).Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4) + .Substitute("d", 0.9).Substitute("n", 0.6); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in new[] { -1.7, -0.6, 0.4, 1.3, 2.2 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + Assert.False(got.IsNaN, $"d/dx of the antiderivative of {integrand} has no value at x = {at}, where the integrand is {want}"); + 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}"); + } + } + } +}