diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 5772f5ada..0ab69148c 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -2315,6 +2315,26 @@ no integrand without one changes: Rubi's independent suites and a sample of its | `"Ei(b*x)^2".Integrate("x")` | `Ei * (b * x) ^ 3 / 3 / b + C`, with `Ei` a variable | an antiderivative in `Ei(b x)` and `Ei(2 b x)` | | `"x*erf(b*x)^2".Integrate("x")` | `UnrecognizedFunctionParseException`: there is no function `erf` | an antiderivative in `erf(b x)` | +### An exponential or a hyperbolic function over several linears is split into partial fractions over them + +`e^x/(x (x + 1))` was left unevaluated, where `e^x/x` and `e^x/(x + 1)` were each answered with +the exponential integral: the rule for an exponential of a linear over a power of a linear read +one linear below the bar, and the rule for `sinh` and `cosh` did the same. The quotient is split +into partial fractions over its linears now, as the rule for a sine or a cosine already split it, +and each term is the one-linear question: `e^x/(x (x + 1))` is `Ei(x) - Ei(x + 1)/e`. It is also +what by parts leaves of `Ei(a + b x)/x^2`, `e^(a + b x)/((a + b x) x)` +([#1501](https://github.com/asc-community/AngouriMath/issues/1501)). The rule for a sine or a cosine +declined a polynomial over linears with a symbol among their coefficients, `x sin(x)/((c + d x)(x - 2))`, +for dividing a fraction that was proper already; neither rule divides one now. Both columns +measured on a build, `v2.5.0` against this change. + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"e^x/(x*(x+1))".Integrate("x")` | `integral(e ^ x / (x * (x + 1)), x)` | `Ei(x) + -1 / e * Ei(x + 1) + C` | +| `"sinh(x)/(x*(x+1))".Integrate("x")` | `integral((e ^ x - e ^ (-x)) / 2 / (x * (x + 1)), x)` | `1/2 * (2 * Shi(x) + ((-1) / e + e) * Chi(x + 1) + ((-1) / e - e) * Shi(x + 1)) + C` | +| `"x^3*e^(2*x)/((x+1)*(x-2))".Integrate("x")` | `integral(x ^ 3 * e ^ (2 * x) / ((x + 1) * (x - 2)), x)` | `e ^ (2 * x) * (-1/4 + 1/2 * x) + e ^ (2 * x) / 2 + 1/3 * e ^ (-2) * Ei(2 * (x + 1)) + 8/3 * e ^ 4 * Ei(2 * (x - 2)) + C` | +| `"x*sin(x)/((c+d*x)*(x-2))".Integrate("x")` | `integral(x * sin(x) / ((c + d * x) * (x - 2)), x)` | in `Si` and `Ci` of `(c + d x)/d` and `x - 2` | + ### An inverse trigonometric function below the bar is integrated to the sine and cosine integrals `1/arcsin(x)` was left unintegrated. Under the substitution that undoes the inverse function, diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index ff88f4070..90ed4521c 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -3859,6 +3859,102 @@ bool Read(Entity factor, int times) } } + /// + /// An exponential of a linear, or a sum of them -- which is how sinh and cosh + /// arrive -- times a polynomial, over two or more linears each to a whole power: split into + /// partial fractions over the linears, and each term the one-linear question of + /// and + /// . e^x/(x (x + 1)) is + /// e^x/x - e^x/(x + 1), which is Ei(x) - Ei(x + 1)/e; the trigonometric rule + /// splits the same way (). It is also what by parts leaves + /// of Ei(a + b x)/x^2, e^(a + b x)/((a + b x) x). + /// https://github.com/asc-community/AngouriMath/issues/1501 + /// + internal static Entity? SolveAnExponentialOverSeveralLinears(Entity expr, Entity.Variable x) + { + if (expr is not (Divf or Mulf) || AnExponentialOfALinear(expr, x) is null && !HasASumOfExponentials(expr, x)) + return null; + Entity constant = Number.Integer.One; + var linears = new List<(Entity Linear, int Power)>(); + Entity? polynomial = null; + Entity? exponentials = null; + foreach (var (factor, underneath) in FactorsOfTheIntegrand(expr)) + { + if (!factor.ContainsNode(x)) + { + constant = underneath ? constant / factor : constant * factor; + continue; + } + var (@base, exponent) = factor is Powf(var raised, Number.Integer whole) && whole.EInteger.CanFitInInt32() && !whole.EInteger.IsZero + ? (raised, whole.EInteger.ToInt32Checked()) : (factor, 1); + if (underneath) + exponent = -exponent; + if (exponent < 0 && TreeAnalyzer.TryGetPolyLinear(@base, x, out var slopeOfIt, out _) && !TreeAnalyzer.IsZero(slopeOfIt)) + { + var at = linears.FindIndex(pair => pair.Linear == @base); + if (at < 0) + linears.Add((@base, -exponent)); + else + linears[at] = (@base, linears[at].Power - exponent); + continue; + } + if (underneath) + return null; + if (factor.Nodes.Any(node => node is Powf(var b, var e) && !b.ContainsNode(x) && e.ContainsNode(x))) + { + exponentials = exponentials is null ? factor : exponentials * factor; + continue; + } + if (TreeAnalyzer.TryGetPolynomial(factor, x, out var monomials) && monomials.Keys.All(degree => degree.Sign >= 0 && degree.CompareTo(EInteger.FromInt32(12)) <= 0)) + { + polynomial = polynomial is null ? factor : polynomial * factor; + continue; + } + return null; + } + if (exponentials is null || linears.Count < 2 || linears.Count > 4 || linears.Sum(pair => pair.Power) > 12) + return null; + // Over each linear by partial fractions, the polynomial part first, since the split is + // of a proper fraction -- as the trigonometric rule does it. Only where the fraction is + // not proper already: divided, `x` over `(c + d x)(x - 2)` came back as a quotient and a + // remainder `2 provided not c + d x = 0`, which nothing splits. + var denominator = linears.Aggregate((Entity)Number.Integer.One, (product, pair) => product * MathS.Pow(pair.Linear, pair.Power)); + Entity numerator = polynomial ?? Number.Integer.One; + var terms = new List(); + if (numerator.ContainsNode(x) + && TreeAnalyzer.TryGetPolynomial(numerator, x, out var monomialsAbove) + && monomialsAbove.Keys.Any(degree => degree.CompareTo(EInteger.FromInt32(linears.Sum(pair => pair.Power))) >= 0) + && TreeAnalyzer.PolynomialLongDivision(numerator, denominator, genericCase: true, inTermsOf: x) is var (quotient, remainder)) + { + if (!TreeAnalyzer.IsZero(quotient)) + terms.Add(quotient); + numerator = remainder is Divf(var left, _) ? left : (numerator - quotient * denominator).Expand().InnerSimplified; + } + if (!TreeAnalyzer.IsZero(numerator)) + { + if (!Functions.PartialFractions.TrySplitOverWrittenFactors(numerator, denominator, x, out var decomposition)) + return null; + var fractions = decomposition.InnerSimplified; + if (fractions is Divf(var over, var by) && !by.ContainsNode(x)) + fractions = Sumf.LinearChildren(over).Aggregate((Entity)Number.Integer.Zero, (all, one) => all + one / by); + terms.AddRange(Sumf.LinearChildren(fractions)); + } + Entity sum = Number.Integer.Zero; + foreach (var term in terms) + { + // Each term as the quotient the one-linear rules read, or, with no linear left, the + // elementary product of a polynomial and the exponentials. + var (above, linear, power) = OverOneLinear(term, x); + var question = linear is null ? above * exponentials : above * exponentials / MathS.Pow(linear, power); + if ((SolveAnExponentialOfALinearOverAPowerOfALinear(question, x) + ?? SolveAHyperbolicOfALinearOverAPowerOfALinear(question, x) + ?? Integration.ComputeIndefiniteIntegral(question, x, false)) is not { } integral) + return null; + sum += integral; + } + return Functions.PartialFractions.Bare((constant * sum).InnerSimplified); + } + /// Whether a factor of is a sum with an exponential of the variable in it. private static bool HasASumOfExponentials(Entity expr, Entity.Variable x) => expr switch { @@ -4542,11 +4638,15 @@ Entity LessTheRoot(Entity root) => ? Functions.PartialFractions.Bare((constant * one).InnerSimplified) : null; // Over each linear by partial fractions, and each term the one-linear question; the - // polynomial part first, since the split is of a proper fraction. + // polynomial part first, since the split is of a proper fraction -- and only where it is + // not proper already, as in SolveAnExponentialOverSeveralLinears. var denominator = linears.Aggregate((Entity)Number.Integer.One, (product, pair) => product * MathS.Pow(pair.Linear, pair.Power)); Entity numerator = polynomial ?? Number.Integer.One; var terms = new List(); - if (numerator.ContainsNode(x) && TreeAnalyzer.PolynomialLongDivision(numerator, denominator, genericCase: true, inTermsOf: x) is var (quotient, remainder)) + if (numerator.ContainsNode(x) + && TreeAnalyzer.TryGetPolynomial(numerator, x, out var monomialsAbove) + && monomialsAbove.Keys.Any(degree => degree.CompareTo(EInteger.FromInt32(linears.Sum(pair => pair.Power))) >= 0) + && TreeAnalyzer.PolynomialLongDivision(numerator, denominator, genericCase: true, inTermsOf: x) is var (quotient, remainder)) { if (!TreeAnalyzer.IsZero(quotient)) terms.Add(quotient); diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 885414428..5be10b342 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -643,6 +643,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => if ((answer = IndefiniteIntegralSolver.SolveAnExponentialOfALinearOverAPowerOfALinear(expr, x)) is { }) return answer; // And a sum of exponentials, which is how sinh and cosh arrive, onto Shi and Chi. if ((answer = IndefiniteIntegralSolver.SolveAHyperbolicOfALinearOverAPowerOfALinear(expr, x)) is { }) return answer; + // And either over several linears, split into partial fractions over them first, as the + // trigonometric rule below splits. + if ((answer = IndefiniteIntegralSolver.SolveAnExponentialOverSeveralLinears(expr, x)) is { }) return answer; // Sines and cosines of a linear over a power of a linear, onto Si and Ci under u = the // linear, where the search would take the quotient by parts without end. if ((answer = IndefiniteIntegralSolver.SolveATrigonometricOfALinearOverAPowerOfALinear(expr, x)) is { }) return answer; diff --git a/Sources/Tests/UnitTests/Calculus/ExponentialIntegralIntegrationTest.cs b/Sources/Tests/UnitTests/Calculus/ExponentialIntegralIntegrationTest.cs index 8a13e3cbb..18fff93ee 100644 --- a/Sources/Tests/UnitTests/Calculus/ExponentialIntegralIntegrationTest.cs +++ b/Sources/Tests/UnitTests/Calculus/ExponentialIntegralIntegrationTest.cs @@ -62,6 +62,20 @@ private static readonly (string, string)[] Pins = public void AnExponentialOfALinearOverAPowerOfALinear(string integrand) => DifferentiatesBack(integrand, AroundZero, Pins); + /// + /// Over several linears, the quotient is split into partial fractions over them first, and + /// each term is the question above: e^x/(x (x + 1)) is Ei(x) - Ei(x + 1)/e. The + /// last row is what by parts leaves of Ei(a + b x)/x^2. + /// + [Theory] + [InlineData("e^x/(x*(x + 1))")] + [InlineData("e^x/(x*(x - a))")] + [InlineData("e^x/(x^2*(x + 1))")] + [InlineData("x^3*e^(2*x)/((x + 1)*(x - 2))")] + [InlineData("e^(a + b*x)/((a + b*x)*x)")] + public void AnExponentialOfALinearOverSeveralLinears(string integrand) + => DifferentiatesBack(integrand, AroundZero, Pins); + /// /// The Gaussian's odd negative moments end at int e^(A x^2)/x = Ei(A x^2)/2, and /// x e^(-1/x^2) is one of them under u = 1/x. diff --git a/Sources/Tests/UnitTests/Calculus/HyperbolicIntegralIntegrationTest.cs b/Sources/Tests/UnitTests/Calculus/HyperbolicIntegralIntegrationTest.cs index 984e30132..7b7282918 100644 --- a/Sources/Tests/UnitTests/Calculus/HyperbolicIntegralIntegrationTest.cs +++ b/Sources/Tests/UnitTests/Calculus/HyperbolicIntegralIntegrationTest.cs @@ -67,6 +67,17 @@ private static readonly (string, string)[] Pins = public void ASumOfExponentialsOverAPowerOfALinear(string integrand) => DifferentiatesBack(integrand, AroundZero, Pins); + /// + /// And over several linears, split into partial fractions over them first, each term the + /// question above. + /// + [Theory] + [InlineData("sinh(x)/(x*(x + 1))")] + [InlineData("cosh(2*x)/(x*(x + 3))")] + [InlineData("x*sinh(a + b*x)/((c + d*x)*(x - 2))")] + public void ASumOfExponentialsOverSeveralLinears(string integrand) + => DifferentiatesBack(integrand, AroundZero, Pins); + /// /// An inverse hyperbolic tangent below the bar, beside a whole power of 1 - a^2 x^2: /// under x = tanh(u)/a it is a polynomial in sinh(u) and cosh(u) over diff --git a/Sources/Tests/UnitTests/Calculus/TrigonometricIntegralIntegrationTest.cs b/Sources/Tests/UnitTests/Calculus/TrigonometricIntegralIntegrationTest.cs index ef129c110..478d248c0 100644 --- a/Sources/Tests/UnitTests/Calculus/TrigonometricIntegralIntegrationTest.cs +++ b/Sources/Tests/UnitTests/Calculus/TrigonometricIntegralIntegrationTest.cs @@ -88,6 +88,8 @@ public void APowerOfASineOrACosineOverAPowerOfALinear(string integrand) [InlineData("cos(2*x + 1)/((x - 1)*(x + 2)^2)")] [InlineData("sin(c + d*x)/(x^2*(a + b*x))")] [InlineData("x^3*sin(x)^2/((x + 1)*(x + 3))")] + [InlineData("x*sin(x)/((c + d*x)*(x - 2))")] + [InlineData("x*cos(a + b*x)/((c + d*x)*(x + 1))")] public void OverSeveralLinears(string integrand) => DifferentiatesBack(integrand, AroundZero, Pins);