diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 277e44c3b..954ee66b5 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -396,6 +396,26 @@ in Rubi's 1.1.3.4 come out shorter as well, and those that took two seconds take | `"1/(x^8*(a + b*x^3)*(c + d*x^3))".ToEntity().Integrate("x")` | `integral(...)` | 1,217 characters | | `"1/(x^6*(a + b*x^4)*(c + d*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 795 characters | +### A trigonometric function of `a + b/x` beside a rational function is integrated in `1/x` + +**Answers where there were none.** `sin(c + d/x)/(e + f x)` was declined: the substitution search reads a candidate's +derivative but never solves the candidate for `x`. Under `x = 1/u` it is `-sin(c + d u)/(u (e u + f)) du`, which the +partial fractions and the sine and cosine integrals answer, and `x = 1/u` is one-to-one on either side of 0. Below a +root of a linear, the linear radical substitution gets there first +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +The answers below a root are large because the sine over the linears that come out of the substitution carries +expanded coefficients ([#1877](https://github.com/asc-community/AngouriMath/issues/1877)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sin(c + d/x)/(e + f*x)".ToEntity().Integrate("x")` | `integral(...)` | 196 characters | +| `"(a + b*sin(c + d/x))/(e + f*x)^2".ToEntity().Integrate("x")` | `integral(...)` | 470 characters | +| `"(a + b*sin(c + d/x))^2/(e + f*x)^3".ToEntity().Integrate("x")` | `integral(...)` | 2,516 characters | +| `"(c*e + d*e*x)^(1/3)*sin(a + b/(c + d*x)^(1/3))".ToEntity().Integrate("x")` | `integral(...)` | 1,453 characters | +| `"sin(a + b/sqrt(c + d*x))/(e + f*x)".ToEntity().Integrate("x")` | `integral(...)` | 12,010 characters | +| `"sin(a + b/sqrt(c + d*x))/(e + f*x)^2".ToEntity().Integrate("x")` | `integral(...)` | 32,780 characters | + ### A power of the cosine beside a secant binomial is integrated in the cosine **Answers where there were none.** `cos(c + d x)^2/(a + b sec(c + d x))` was declined, and so were the cosine's powers diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index dfca3fe23..7d0e7556d 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -19120,6 +19120,77 @@ Entity TakeOut(Entity node) /// Set while asks its question. [System.ThreadStatic] private static bool takingTheWholePowersOut; + /// + /// A rational function of x and of trigonometric functions of a + b/x, under + /// x = 1/u: sin(c + d/x)/(e + f x) is + /// -sin(c + d u)/(u (e u + f)) du, which the partial fractions and the sine and + /// cosine integrals answer. + /// + /// + /// x = 1/u is one-to-one on either side of 0, so the answer in u written back + /// in 1/x holds on both. Every argument of a function holding x has to be + /// linear in 1/x and not in x, so the integrand in u is one this rule + /// does not read again. Rubi's 4.1.12 (a + b sin(c + d/x))^k/(e + f x)^m were + /// declined: the substitution search reads a candidate's derivative but never solves + /// the candidate for x. https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveByTheReciprocalOfAFunctionOfTheReciprocal(Entity expr, Entity.Variable x, bool integrateByParts) + { + var u = Variable.CreateUnique(expr, "u_rec"); + // Each function of x, named, and written in u: its argument the linear in u it is. + var inU = new Dictionary(); + var names = new Dictionary(); + foreach (var node in expr.Nodes) + { + Entity argument; + switch (node) + { + case Sinf(var a): argument = a; break; + case Cosf(var a): argument = a; break; + case Tanf(var a): argument = a; break; + case Cotanf(var a): argument = a; break; + case Secantf(var a): argument = a; break; + case Cosecantf(var a): argument = a; break; + case Powf(_, Number.Integer): + case Sumf or Minusf or Mulf or Divf or Variable or Number: + continue; + default: + if (node.ContainsNode(x)) + return null; + continue; + } + if (!argument.ContainsNode(x) || inU.ContainsKey(node)) + continue; + if (!TreeAnalyzer.TryGetPolynomial(argument.Substitute(x, Number.Integer.One / u).InnerSimplified, u, out var read) + || read.Keys.Any(k => k.Sign < 0 || k.CompareTo(EInteger.One) > 0) || !read.TryGetValue(EInteger.One, out var slope)) + return null; + Entity linear = read.TryGetValue(EInteger.Zero, out var constant) + ? constant.InnerSimplified + slope.InnerSimplified * u + : slope.InnerSimplified * u; + inU[node] = node switch + { + Sinf => MathS.Sin(linear), + Cosf => MathS.Cos(linear), + Tanf => MathS.Tan(linear), + Cotanf => MathS.Cotan(linear), + Secantf => MathS.Sec(linear), + Cosecantf => MathS.Cosec(linear), + _ => node, + }; + names[node] = Variable.CreateUnique(expr, "f_rec" + names.Count); + } + if (inU.Count == 0) + return null; + // The rest one quotient, the way the rules read it: not `e + f/u` but `(e u + f)/u`. + var named = expr.Replace(node => names.TryGetValue(node, out var name) ? name : node); + var integrand = Functions.SingleQuotient.Combine(-named.Substitute(x, Number.Integer.One / u) / MathS.Sqr(u)).InnerSimplified; + foreach (var pair in names) + integrand = integrand.Substitute(pair.Value, inU[pair.Key]); + if (Integration.ComputeIndefiniteIntegral(integrand, u, integrateByParts) is not { } inTermsOfU) + return null; + return inTermsOfU.Substitute(u, Number.Integer.One / x); + } + /// /// A root of a product beside a root of one of its factors, the product split: /// (P Q)^r is K P^r Q^r, where K = (P Q)^r/(P^r Q^r) is constant wherever diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index b4c2725ba..bf2c21f94 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -666,6 +666,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => if ((answer = IndefiniteIntegralSolver.SolveAGaussianInAReciprocal(expr, x)) is { }) return answer; // And a trigonometric function of the reciprocal of a linear beside a power of it, under u = 1/L. if ((answer = IndefiniteIntegralSolver.SolveAFunctionOfTheReciprocalOfALinear(expr, x, integrateByParts)) is { }) return answer; + // And one of `a + b/x` beside any rational function, under `x = 1/u`: + // `sin(c + d/x)/(e + f x)` is `-sin(c + d u)/(u (e u + f)) du`. + if ((answer = IndefiniteIntegralSolver.SolveByTheReciprocalOfAFunctionOfTheReciprocal(expr, x, integrateByParts)) is { }) return answer; // An exponential of a multiple of a logarithm is a power of the argument, which is // how every inverse hyperbolic function under an exponential arrives. if ((answer = IndefiniteIntegralSolver.SolveByFoldingAnExponentialOfALogarithm(expr, x, integrateByParts)) is { }) return answer; diff --git a/Sources/Tests/UnitTests/Calculus/AFunctionOfTheReciprocalIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/AFunctionOfTheReciprocalIntegralTest.cs new file mode 100644 index 000000000..c06b1d265 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AFunctionOfTheReciprocalIntegralTest.cs @@ -0,0 +1,79 @@ +// +// 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 trigonometric function of a + b/x beside a rational function, under x = 1/u: + /// sin(c + d/x)/(e + f x) is -sin(c + d u)/(u (e u + f)) du. + /// #718 + /// + /// + /// Rubi's 4.1.12 (a + b sin(c + d/x))^k/(e + f x)^m were declined. Each answer is + /// differentiated back on both sides of 0. + /// + [Trait("Area", "Calculus")] + public sealed class AFunctionOfTheReciprocalIntegralTest + { + private static void DifferentiatesBack(string integrand, double[] points, params (string, double)[] pins) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Assert.DoesNotContain("NaN", integral.Stringize()); + + var derivative = integral.Substitute("C", 0).Differentiate("x"); + Entity original = integrand.ToEntity(); + foreach (var (name, value) in pins) + { + derivative = derivative.Substitute(name, value); + original = original.Substitute(name, value); + } + + var compared = 0; + foreach (var at in points) + { + var got = derivative.Substitute("x", at).EvalNumerical(); + var want = original.Substitute("x", at).EvalNumerical(); + if (got.IsNaN || want.IsNaN) + continue; + compared++; + var difference = Math.Abs((double)(got - want).RealPart) + + Math.Abs((double)(got - want).ImaginaryPart); + var scale = Math.Max(1.0, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)); + Assert.True(difference / scale < 1e-8, + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, " + + $"where the integrand is {want}"); + } + Assert.True(compared >= 3, + $"only {compared} of {points.Length} points were comparable for {integrand}"); + } + + [Theory] + [InlineData("sin(c+d/x)/(e+f*x)")] + [InlineData("cos(c+d/x)/(e+f*x)^2")] + [InlineData("(a+b*sin(c+d/x))/(e+f*x)^3")] + [InlineData("(a+b*sin(c+d/x))^2/(e+f*x)")] + public void AFunctionOfAPlusBOverX(string integrand) + { + var points = new[] { -1.4, -0.3, 0.8, 2.25 }; + DifferentiatesBack(integrand, points, ("a", 1.0 / 3), ("b", 2), ("c", 1.5), ("d", 1.25), ("e", 2), ("f", 3)); + } + + [Theory] + [InlineData("(c*e+d*e*x)^(1/3)*sin(a+b/(c+d*x)^(1/3))")] + [InlineData("sin(a+b/sqrt(c+d*x))/(e+f*x)")] + public void AFunctionOfAPlusBOverARootOfALinear(string integrand) + { + var points = new[] { -0.3, 0.8, 2.25 }; + DifferentiatesBack(integrand, points, ("a", 1.0 / 3), ("b", 2), ("c", 1.5), ("d", 1.25), ("e", 2), ("f", 3)); + } + } +}