diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index e3cd35982..d7101894e 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -59,9 +59,21 @@ internal static partial class Integration if ((answer = IndefiniteIntegralSolver.SolveLogarithmic(expr, x, integrateByParts)) is { }) return answer; if ((answer = IndefiniteIntegralSolver.SolveBySubstitution(expr, x, integrateByParts)) is { }) return answer; if ((answer = IndefiniteIntegralSolver.SolveByPartialFractions(expr, x, integrateByParts)) is { }) return answer; - if (integrateByParts && (answer = IndefiniteIntegralSolver.SolveIntegratingByParts(expr, x)) is { }) return answer; - // this may expand to too many terms + // Linearity comes before integration by parts, because it decomposes the problem + // into strictly simpler ones where by parts searches. It cannot cost an answer: + // splitting returns null unless *every* term integrates, so a sum that only comes + // out whole still falls through to by parts below. + // + // A product with a sum in it -- sin(a+f*x)^4 * (5 - 6*sin(a+f*x)^2) -- reaches + // this as a product, so only the expansion here finds the two terms it is. Behind + // by parts it never did: the search spent the whole budget first and the caller + // waited over 20 seconds to be told nothing, where the split answers in 0.16. + // https://github.com/asc-community/AngouriMath/issues/779 + // + // Expansion is bounded by MaxExpansionTermCount, which returns null rather than + // building the terms, so putting it earlier cannot blow the tree up either. if ((answer = IndefiniteIntegralSolver.SolveBySplittingSum(expr, x, integrateByParts)) is { }) return answer; + if (integrateByParts && (answer = IndefiniteIntegralSolver.SolveIntegratingByParts(expr, x)) is { }) return answer; return null; } /// diff --git a/Sources/Tests/UnitTests/Calculus/LinearityBeforeByPartsTest.cs b/Sources/Tests/UnitTests/Calculus/LinearityBeforeByPartsTest.cs new file mode 100644 index 000000000..47955617f --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/LinearityBeforeByPartsTest.cs @@ -0,0 +1,118 @@ +// +// 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 System; +using System.Threading.Tasks; +using AngouriMath; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A product with a sum inside it -- sin(a+f*x)^4 * (5 - 6*sin(a+f*x)^2) -- is the + /// same function as the expanded difference, which answers in milliseconds. Written + /// factored it did not finish in twenty seconds, because the only rule that would + /// distribute it ran behind integration by parts and the search spent the whole budget + /// first. https://github.com/asc-community/AngouriMath/issues/779 + /// + /// Every answer is checked by differentiating it back and comparing at points: what + /// matters is that it is an antiderivative, not what form it is written in. + /// + public sealed class LinearityBeforeByPartsTest + { + /// + /// Generous enough that a slow machine will not fail it, and far below the twenty + /// seconds this shape used to spend. Asserts termination rather than speed -- a + /// regression here is a hang, and a hang would otherwise wedge the run instead of + /// failing it. + /// + private static readonly TimeSpan Budget = TimeSpan.FromSeconds(15); + + private static Entity IntegrateWithinBudget(string integrand) + { + Entity? answer = null; + var task = Task.Run(() => answer = integrand.ToEntity().Integrate("x")); + Assert.True(task.Wait(Budget), $"{integrand} did not finish within {Budget}"); + return answer!; + } + + private static void AssertIsAntiderivative(string integrand, params double[] points) + { + var antiderivative = IntegrateWithinBudget(integrand); + Assert.DoesNotContain("integral(", antiderivative.Stringize()); + var f = integrand.ToEntity().Substitute("a", 0.7).Substitute("f", 1.3); + var derivative = antiderivative + .Substitute("C", 0).Substitute("a", 0.7).Substitute("f", 1.3) + .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, 7); + } + } + + /// + /// The integrand from the issue, and the neighbours that make the point: written as a + /// difference it always answered, and the factored form is the same function. + /// + [Theory] + [InlineData("sin(a + f * x) ^ 4 * (5 - 6 * sin(a + f * x) ^ 2)", new[] { 0.4, 1.6, -0.9 })] + [InlineData("5 * sin(a + f * x) ^ 4 - 6 * sin(a + f * x) ^ 6", new[] { 0.4, 1.6, -0.9 })] + [InlineData("sin(a + f * x) ^ 2 * (1 + sin(a + f * x) ^ 2)", new[] { 0.4, 1.6 })] + public void AProductWithASumInsideIsDistributed(string integrand, double[] points) => + AssertIsAntiderivative(integrand, points); + + /// + /// Linearity is not specific to trigonometry, and these are the shapes most likely to + /// be disturbed by moving it in front of integration by parts. + /// + [Theory] + [InlineData("x * (1 + x)", new[] { 0.4, 1.6, -2.2 })] + [InlineData("(x + 1) * (x + 2)", new[] { 0.4, 1.6, -2.2 })] + [InlineData("cos(x) * (1 + x)", new[] { 0.4, 1.6, -2.2 })] + [InlineData("e ^ x * (1 + x)", new[] { 0.4, 1.6, -2.2 })] + public void OrdinaryProductsOverSumsStillAnswer(string integrand, double[] points) => + AssertIsAntiderivative(integrand, points); + + /// + /// Integration by parts is still reached. Splitting returns nothing unless *every* + /// term integrates, so a sum that only comes out whole falls through to it -- which is + /// what makes putting linearity first unable to cost an answer. + /// + [Theory] + [InlineData("x * cos(x)", new[] { 0.4, 1.6, -2.2 })] + [InlineData("x * ln(x)", new[] { 0.4, 1.6 })] + [InlineData("x ^ 2 * e ^ x", new[] { 0.4, 1.6, -2.2 })] + public void ByPartsIsStillReached(string integrand, double[] points) => + AssertIsAntiderivative(integrand, points); + + /// + /// **A separate known gap, pinned here so it is not read as this one.** Two powers of + /// one base are never merged: x^2 / x is declined though it is x, and + /// distributing the first case below gives sin(x)^4 * (-6) * sin(x)^2, which + /// is declined though it is -6 sin(x)^6. + /// + /// It is only ever fixed by accident. A substitution rebuilds the tree on its way + /// past, and that rebuild is what merges the powers -- which is why every argument + /// other than a bare x (2*x, x+1, a+f*x) answers and a + /// bare x does not. + /// + /// These are declined in milliseconds rather than hung, so this is a missing answer + /// and not the defect this file is about. Measured on master before the reordering, + /// so it is pre-existing rather than caused by it. + /// + [Theory] + [InlineData("sin(x) ^ 4 * (5 - 6 * sin(x) ^ 2)")] + [InlineData("sin(x) ^ 4 * (-6) * sin(x) ^ 2")] + [InlineData("x ^ 2 * (x + 1 / x)")] + [InlineData("x ^ 2 / x")] + public void PowersOfOneBaseAreNotMerged(string integrand) => + Assert.Contains("integral(", IntegrateWithinBudget(integrand).Stringize()); + } +}