diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 6faf6c53c..1db61d4ea 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -390,6 +390,23 @@ polynomial exponent in `u` now, and the question asked in `u`. Rubi's 2.3 | `"F^(a + b/(c + d*x))/(c + d*x)".ToEntity().Integrate("x")` | `integral(...)` | an exponential integral | | `"F^(a + b/(c + d*x)^3)*(c + d*x)^2".ToEntity().Integrate("x")` | `integral(...)` | an exponential integral of the cube, and the exponential | +### A function of a quotient of two linears is integrated over the quotient's denominator + +**Answers where there were none.** `sin((a + b x)/(c + d x))` and its powers were declined, and so +were the cosine's, the hyperbolic sine's and cosine's and the exponential of the same quotient, +where `sin(p + k/(c + d x))` is answered in the sine and cosine integrals and `e^(p + k/(c + d x))` +in the exponential integral. The quotient is one of those: `b/d + (a d - b c)/(d (c + d x))`, by +polynomial division, for `d` and `a d - b c` not zero. Each such argument is written so and the +question asked again. Rubi's 4.7.7, 6.1.5 and 6.2.5 +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sin((a + b*x)/(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | in the sine and cosine integrals of `(a d - b c)/(d (c + d x))` | +| `"cos((a + b*x)/(c + d*x))^2".ToEntity().Integrate("x")` | `integral(...)` | in the sine and cosine integrals of twice that | +| `"sinh((a + b*x)/(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | in the exponential integrals of `±(a d - b c)/(d (c + d x))` | +| `"e^((a + b*x)/(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | in the exponential integral of `(a d - b c)/(d (c + d x))` | + ### A polynomial over a power of a binomial past the cube is integrated **Answers where there were none.** `P(x)/(a + b x^n)^k` with symbols in the binomial, `n >= 3`, was diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 33b6f88f1..a0bcb9070 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -11418,6 +11418,97 @@ static bool IsEvenIn(Entity polynomial, Entity.Variable even, Entity.Variable ot return !inPlaceholder.ContainsNode(x) && TreeAnalyzer.TryGetPolynomial(inPlaceholder, placeholder, out _); } + /// + /// A function whose argument is a quotient of two linears, (a + b x)/(c + d x), with + /// that argument written over the denominator: b/d + (a d - b c)/(d (c + d x)), a + /// constant plus a multiple of the reciprocal of a linear. + /// + /// + /// + /// Rubi's sin((a + b x)/(c + d x)) and its powers were declined, and the hyperbolic + /// sine and cosine of the same, written in exponentials, likewise, where + /// sin(a + k/(c + d x)) is answered in sines and cosine integrals and + /// e^(a + k/(c + d x)) in exponential integrals: those rules read a reciprocal of a + /// linear, and a quotient of two linears is one plus a constant. Exact: polynomial division, + /// for d and a d - b c not zero, as everywhere in the integrator. + /// + /// + /// The arguments of the sine, cosine, tangent, cotangent, secant and cosecant and the + /// exponents of the exponential, each written so; the same question in another spelling. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + /// + internal static Entity? SolveByWritingAQuotientOfLinearsOverItsDenominator(Entity expr, Entity.Variable x, bool integrateByParts) + { + var names = new Dictionary(); + Entity? Divided(Entity argument) + { + // Written as a quotient, or a constant times one: the sum this writes is neither, + // so the same question asked again does not divide it a second time. + Entity factor = Number.Integer.One; + var quotient = argument; + // The product's two children as written: read through, `-(a + b x)/(c + d x)` as a + // constant times a quotient is a quotient taken apart into its factors. + if (argument is Mulf(var left, var right)) + (factor, quotient) = left.ContainsNode(x) ? (right, left) : (left, right); + if (factor.ContainsNode(x)) + return null; + if (quotient is not Divf(var above, var below)) + return null; + if (!below.ContainsNode(x) || !above.ContainsNode(x) + || !TreeAnalyzer.TryGetPolyLinear(above, x, out var b, out var a) || b.ContainsNode(x) || a.ContainsNode(x) + || !TreeAnalyzer.TryGetPolyLinear(below, x, out var d, out var c) || d.ContainsNode(x) || c.ContainsNode(x)) + return null; + var remainder = (a * d - b * c).InnerSimplified; + if (remainder.Evaled is Number.Complex { IsZero: true } || d.Evaled is Number.Complex { IsZero: true }) + return null; + // The constant and the multiple named, each a symbol of its own while the question + // is asked: `sin(b/d + ((a d - b c)/d)/(c + d x))` was declined where + // `sin(p + k/(c + d x))` is answered, the rules reading a symbol where they meet a + // quotient of symbols. + Entity Named(Entity value) + { + value = value.InnerSimplified; + if (value is Variable || value is Number) + return value; + foreach (var pair in names) + if (pair.Value == value) + return pair.Key; + var name = Variable.CreateUnique(expr + names.Keys.Aggregate((Entity)Number.Integer.Zero, (sum, v) => sum + v), "k_quotient"); + names[name] = value; + return name; + } + // A constant in front stays in front, so that `e^Q` and `e^(-Q)` keep one argument + // and their product is 1. + var divided = Named(b / d) + Named(remainder / d) / below; + return factor == Number.Integer.One ? divided : factor * divided; + } + var changed = false; + var rewritten = expr.Replace(node => + { + Entity? divided; + switch (node) + { + case Sinf(var y) when (divided = Divided(y)) is not null: changed = true; return MathS.Sin(divided); + case Cosf(var y) when (divided = Divided(y)) is not null: changed = true; return MathS.Cos(divided); + case Tanf(var y) when (divided = Divided(y)) is not null: changed = true; return MathS.Tan(divided); + case Cotanf(var y) when (divided = Divided(y)) is not null: changed = true; return MathS.Cotan(divided); + case Secantf(var y) when (divided = Divided(y)) is not null: changed = true; return MathS.Sec(divided); + case Cosecantf(var y) when (divided = Divided(y)) is not null: changed = true; return MathS.Cosec(divided); + case Powf(var @base, var exponent) when @base == MathS.e && (divided = Divided(exponent)) is not null: + changed = true; + return MathS.Pow(MathS.e, divided); + default: + return node; + } + }); + if (!changed || Integration.ComputeAsTheSameQuestion(rewritten, x, integrateByParts) is not { } answer) + return null; + foreach (var pair in names) + answer = answer.Substitute(pair.Key, pair.Value); + return answer; + } + /// /// Trigonometric functions of several arguments that are whole or rational multiples of one /// linear, p + q x, written in u = p + q x: csc(a + b x) csc(2a + 2b x)^2 diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 364c40dfc..91f5c832d 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -876,13 +876,16 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => if ((answer = IndefiniteIntegralSolver.SolveByTheHalfAngleWhereOnePlusAHyperbolicCosineIsASquare(expr, x, integrateByParts)) is { }) return answer; // The hyperbolic sine's, off the real line: 1 + i sinh(y) is (cosh(y/2) + i sinh(y/2))^2. if ((answer = IndefiniteIntegralSolver.SolveAHalfOddPowerOfOnePlusAnImaginaryHyperbolicSine(expr, x, integrateByParts)) is { }) return answer; - // Several trigonometric arguments that are multiples of one linear with an offset or a - // symbolic slope, written in that linear: before the substitution search, which reads - // each function on its own. + // A quotient of linears as a function's argument, written over its denominator: a + // constant plus a multiple of the reciprocal of a linear, which the rules read. + if ((answer = IndefiniteIntegralSolver.SolveByWritingAQuotientOfLinearsOverItsDenominator(expr, x, integrateByParts)) is { }) return answer; // A cosine and a sine over a power of another such sum, through the denominator, its // derivative and a constant, down to the reciprocal of the base: before the substitution // search, which reads the quotient term by term. if ((answer = IndefiniteIntegralSolver.SolveACosineAndASineOverAPowerOfAnother(expr, x, integrateByParts)) is { }) return answer; + // Several trigonometric arguments that are multiples of one linear with an offset or a + // symbolic slope, written in that linear: before the substitution search, which reads + // each function on its own. if ((answer = IndefiniteIntegralSolver.SolveByWritingMultiplesOfOneLinearArgument(expr, x, integrateByParts)) is { }) return answer; // Two tangents, cotangents, secants or cosecants of arguments a constant apart, written // as functions of each alone by the addition formulas. diff --git a/Sources/Tests/UnitTests/Calculus/QuotientOfLinearsAsAnArgumentIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/QuotientOfLinearsAsAnArgumentIntegralTest.cs new file mode 100644 index 000000000..24f73c174 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/QuotientOfLinearsAsAnArgumentIntegralTest.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 sine, cosine or exponential of a quotient of two linears, (a + b x)/(c + d x), + /// written as the constant b/d plus (a d - b c)/d over c + d x, and + /// answered in the sine, cosine and exponential integrals. Rubi's 4.7.7, 6.1.5 and 6.2.5. + /// #718 + /// + /// + /// Checked by differentiating back with a = 0.3, b = 0.9, c = 1.1, + /// d = 0.7, on both sides of the pole at c + d x = 0. + /// + [Trait("Area", "Calculus")] + public sealed class QuotientOfLinearsAsAnArgumentIntegralTest + { + [Theory] + [InlineData("sin((a + b*x)/(c + d*x))")] + [InlineData("sin((a + b*x)/(c + d*x))^2")] + [InlineData("sin((a + b*x)/(c + d*x))^3")] + [InlineData("cos((a + b*x)/(c + d*x))")] + [InlineData("cos((a + b*x)/(c + d*x))^2")] + [InlineData("sinh((a + b*x)/(c + d*x))")] + [InlineData("sinh((a + b*x)/(c + d*x))^3")] + [InlineData("cosh((a + b*x)/(c + d*x))^2")] + [InlineData("e^((a + b*x)/(c + d*x))")] + [InlineData("sin(2*(a + b*x)/(c + d*x))")] + public void IsWrittenOverTheDenominator(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Entity Pinned(Entity e) => e.Substitute("a", 0.3).Substitute("b", 0.9).Substitute("c", 1.1).Substitute("d", 0.7); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in new[] { -2.5, -2.0, 0.3, 0.8, 1.4 }) + { + 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}"); + } + } + } +}