diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs index 6211b028c..b08b071d8 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs @@ -66,7 +66,7 @@ _ when TryReadSineCosinePowers(expr, x, out var trigArg, out var sinePower, out (arg / a) * (MathS.Ln(MathS.Abs(arg)) - 1), Entity.Divf(var numerator, var denominator) when - !numerator.ContainsNode(x) + !numerator.ContainsNode(x) && TreeAnalyzer.TryGetPolyQuadratic(denominator, x, out var a, out var b, out var c) // ∫ k/(ax^2 + bx + c) dx => IntegrateRationalQuadratic(numerator, a, b, c, x), @@ -132,6 +132,42 @@ _ when TryReadSineCosinePowers(expr, x, out var trigArg, out var sinePower, out && TreeAnalyzer.TryGetPolyQuadratic(radicand, x, out var ra, out var rb, out var rc) => IntegrateOverRootOfQuadratic(numerator, ra, rb, rc, radicand, x), + // ∫ (px + q)/(ax^2 + bx + c) dx. Only the constant numerator was covered, so + // x/(x^2 + 2x + 5) and (x + 3)/(x^2 + 3x + 2) had no antiderivative at all. + Entity.Divf(var numerator, var denominator) when + numerator.ContainsNode(x) + && TreeAnalyzer.TryGetPolyLinear(numerator, x, out var p, out var q) + && TreeAnalyzer.TryGetPolyQuadratic(denominator, x, out var a, out var b, out var c) + && a.Evaled is Entity.Number.Complex { IsZero: false } + => IntegrateLinearOverQuadratic(p, q, a, b, c, denominator, x), + + // ∫ (px + q)/(bx + c) dx, the same rewrite one degree down: the quotient is the + // constant p/b plus a remainder over the divisor. x/(x + 1) had no antiderivative. + Entity.Divf(var numerator, var denominator) when + numerator.ContainsNode(x) + && TreeAnalyzer.TryGetPolyLinear(numerator, x, out var p, out var q) + && TreeAnalyzer.TryGetPolyLinear(denominator, x, out var b, out var c) + && b.Evaled is Entity.Number.Complex { IsZero: false } + => p * x / b + (q - p * c / b) * MathS.Ln(MathS.Abs(denominator)) / b, + + // 1/cos(u)^2 and 1/sin(u)^2, which are written that way at least as often as + // sec(u)^2 and csc(u)^2 and were not recognised in that form. + Entity.Divf(var numerator, Entity.Powf(Entity.Cosf(var arg), Entity.Number.Integer(2))) when + !numerator.ContainsNode(x) && TreeAnalyzer.TryGetPolyLinear(arg, x, out var a, out _) => + numerator * MathS.Tan(arg) / a, + + Entity.Divf(var numerator, Entity.Powf(Entity.Sinf(var arg), Entity.Number.Integer(2))) when + !numerator.ContainsNode(x) && TreeAnalyzer.TryGetPolyLinear(arg, x, out var a, out _) => + -numerator * MathS.Cotan(arg) / a, + + Entity.Powf(Entity.Secantf(var arg), Entity.Number.Integer(2)) when + TreeAnalyzer.TryGetPolyLinear(arg, x, out var a, out _) => + MathS.Tan(arg) / a, + + Entity.Powf(Entity.Cosecantf(var arg), Entity.Number.Integer(2)) when + TreeAnalyzer.TryGetPolyLinear(arg, x, out var a, out _) => + -MathS.Cotan(arg) / a, + _ => null }; @@ -311,6 +347,17 @@ private static Entity IntegrateOverRootOfQuadratic( ]); } + /// + /// ∫ (px + q)/(ax^2 + bx + c) dx, by writing the numerator as a multiple of the + /// denominator's derivative plus a constant: + /// px + q = (p/2a)(2ax + b) + (q - pb/2a). The first part integrates to a logarithm + /// and the second is the constant-numerator case above. + /// + private static Entity IntegrateLinearOverQuadratic( + Entity p, Entity q, Entity a, Entity b, Entity c, Entity denominator, Entity.Variable x) + => p / (2 * a) * MathS.Ln(MathS.Abs(denominator)) + + IntegrateRationalQuadratic(q - p * b / (2 * a), a, b, c, x); + private static Entity IntegrateRationalQuadratic(Entity numerator, Entity a, Entity b, Entity c, Entity.Variable x) { // The formula depends on whether it's linear (a = 0) or quadratic (a ≠ 0) diff --git a/Sources/Tests/UnitTests/Calculus/RationalIntegralsTest.cs b/Sources/Tests/UnitTests/Calculus/RationalIntegralsTest.cs new file mode 100644 index 000000000..d1baf7c6a --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/RationalIntegralsTest.cs @@ -0,0 +1,81 @@ +// +// Copyright (c) 2019-2022 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using AngouriMath; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// Quotients of polynomials of low degree. Only a constant numerator was recognised, so + /// x/(x^2 + 2x + 5) and x/(x + 1) had no antiderivative at all. Each answer is checked by + /// differentiating it back and comparing at points, since what matters is that it is an + /// antiderivative and not what form it is written in. + /// + public sealed class RationalIntegralsTest + { + private static void AssertIsAntiderivative(string integrand, params double[] points) + { + var f = integrand.ToEntity(); + var antiderivative = f.Integrate("x"); + Assert.DoesNotContain("integral(", antiderivative.Stringize()); + var derivative = antiderivative.Substitute("C", 0).Differentiate("x"); + foreach (var point in points) + { + var expected = f.Substitute("x", point).EvalNumerical().RealPart.EDecimal.ToDouble(); + var actual = derivative.Substitute("x", point).EvalNumerical().RealPart.EDecimal.ToDouble(); + Assert.Equal(expected, actual, 8); + } + } + + // (px + q) / (ax^2 + bx + c). The numerator is written as a multiple of the + // denominator's derivative plus a constant, which splits it into a logarithm and the + // constant-numerator case that was already there. + [Theory] + [InlineData("x / (x ^ 2 + 2 * x + 5)", new[] { 0.3, 1.7, -0.6 })] + [InlineData("(x + 3) / (x ^ 2 + 3 * x + 2)", new[] { 0.3, 1.7, 4.2 })] + [InlineData("(2 * x + 1) / (x ^ 2 + x + 1)", new[] { 0.3, 1.7, -0.6 })] + [InlineData("x / (x ^ 2 + 1)", new[] { 0.3, 1.7, -0.6 })] + [InlineData("(3 * x - 2) / (2 * x ^ 2 + 5)", new[] { 0.3, 1.7, -0.6 })] + public void ALinearNumeratorOverAQuadratic(string integrand, double[] points) => + AssertIsAntiderivative(integrand, points); + + // (px + q) / (bx + c), the same rewrite one degree down. + [Theory] + [InlineData("x / (x + 1)", new[] { 0.3, 1.7 })] + [InlineData("(2 * x + 1) / (x - 3)", new[] { 0.3, 1.7 })] + [InlineData("(3 * x) / (2 * x + 5)", new[] { 0.3, 1.7 })] + public void ALinearNumeratorOverALinearDenominator(string integrand, double[] points) => + AssertIsAntiderivative(integrand, points); + + // 1/cos(u)^2 and 1/sin(u)^2 are written that way at least as often as sec(u)^2 and + // csc(u)^2, and neither of the four shapes was recognised. + [Theory] + [InlineData("1 / cos(x) ^ 2", new[] { 0.3, 1.1, -0.6 })] + [InlineData("1 / sin(x) ^ 2", new[] { 0.3, 1.1, 2.2 })] + [InlineData("2 / cos(3 * x) ^ 2", new[] { 0.3, 0.9 })] + [InlineData("sec(x) ^ 2", new[] { 0.3, 1.1, -0.6 })] + [InlineData("cosec(x) ^ 2", new[] { 0.3, 1.1, 2.2 })] + public void ReciprocalSquaresOfTheWaves(string integrand, double[] points) => + AssertIsAntiderivative(integrand, points); + + // The shapes these sit next to in the table have to keep working. A constant over a + // quadratic in particular is matched by the earlier arm and must stay there. + [Theory] + [InlineData("1 / (x ^ 2 + 1)", new[] { 0.3, 1.7, -0.6 })] + [InlineData("1 / (x ^ 2 + 2 * x + 5)", new[] { 0.3, 1.7, -0.6 })] + [InlineData("1 / (2 * x + 3)", new[] { 0.3, 1.7 })] + [InlineData("1 / x", new[] { 0.3, 1.7 })] + [InlineData("x ^ 2", new[] { 0.3, 1.7 })] + [InlineData("sin(x)", new[] { 0.3, 1.7 })] + [InlineData("tan(x)", new[] { 0.3, 1.1 })] + [InlineData("x * e ^ x", new[] { 0.3, 1.7 })] + public void NeighbouringFormsAreUnaffected(string integrand, double[] points) => + AssertIsAntiderivative(integrand, points); + } +}