Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
/// <summary>
Expand Down
118 changes: 118 additions & 0 deletions Sources/Tests/UnitTests/Calculus/LinearityBeforeByPartsTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A product with a sum inside it -- <c>sin(a+f*x)^4 * (5 - 6*sin(a+f*x)^2)</c> -- 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
/// <para/>
/// 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.
/// </summary>
public sealed class LinearityBeforeByPartsTest
{
/// <summary>
/// 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.
/// </summary>
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);
}
}

/// <summary>
/// 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.
/// </summary>
[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);

/// <summary>
/// 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.
/// </summary>
[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);

/// <summary>
/// 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.
/// </summary>
[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);

/// <summary>
/// **A separate known gap, pinned here so it is not read as this one.** Two powers of
/// one base are never merged: <c>x^2 / x</c> is declined though it is <c>x</c>, and
/// distributing the first case below gives <c>sin(x)^4 * (-6) * sin(x)^2</c>, which
/// is declined though it is <c>-6 sin(x)^6</c>.
/// <para/>
/// 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 <c>x</c> (<c>2*x</c>, <c>x+1</c>, <c>a+f*x</c>) answers and a
/// bare <c>x</c> does not.
/// <para/>
/// 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.
/// </summary>
[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());
}
}
Loading