diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index cc161e3d1..747dd3269 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -75,6 +75,19 @@ not seen, and a symmetric divergent one comes to its principal value | `"ln(x)".ToEntity().DefiniteIntegral("x", 0, 1)` | `NaN` | `-1.00000` | | `"1/x".ToEntity().DefiniteIntegral("x", 0, 1)` | `NaN` | `NaN` | +### A definite integral whose bound has no decided limit is left as written + +**Not silent: what was `NaN` is the integral as written.** Where the antiderivative is undefined at a bound, its limit +from inside the range is taken, and where that limit could not be decided the integral was `NaN`, which says it does not +exist. It is left as written now. The limit at a bound is also asked within a budget of the limits it takes along the way, +so that a simplification asking it for each spelling of the integrand ends. +([#2081](https://github.com/asc-community/AngouriMath/issues/2081)) + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"integral(ln(x) * erf(x), x, 0, 1)".ToEntity().Simplify()` | `NaN` | `integral(erf(x) * ln(x), x, 0, 1)`, whose value is `-0.2613…` | +| `"integral(ln(x) * Si(x), x, 0, 1)".ToEntity().Simplify()` | `NaN` | `integral(Si(x) * ln(x), x, 0, 1)`, whose value is `-0.2466…` | + ### A root of a small or a large number is searched for at the size of one **Silent: wrong answers.** The search for an exact root of a number rounded each candidate to six digits, absolutely. diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 137b9d2b4..5849f7600 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -121,9 +121,10 @@ private static bool HoldsAnIncompleteGammaFunction(Entity expr) /// at 0 is 0 * -oo, so the integral of ln(x) from 0 to 1 was NaN where it /// is -1. The limit is taken only where the value is undefined, so a bound the substitution /// answered is answered as before. Its side is known at an infinity, and at a finite bound - /// from the other bound where both have numeric values. Elsewhere, and where the limit is not - /// decided, the value stays undefined, as it is for sin(x) from 0 to +oo, - /// whose antiderivative has no limit there. + /// from the other bound where both have numeric values. Elsewhere, and where the limit is + /// undefined, the value stays undefined, as it is for sin(x) from 0 to +oo, + /// whose antiderivative has no limit there. Where the limit is not decided there is no value, + /// , and the integral is left as written. /// private static Entity? AtTheBound(Entity antiderivative, Variable x, Entity bound, Entity otherBound) { @@ -147,9 +148,51 @@ private static bool HoldsAnIncompleteGammaFunction(Entity expr) // and evaluating Gamma(3/4) at every step, for over a minute. https://github.com/asc-community/AngouriMath/issues/2075 if (HoldsAnIncompleteGammaFunction(antiderivative)) return null; - var limit = antiderivative.Limit(x, bound, inside); - return limit.IsNaN || limit.Nodes.Any(node => node is Limitf) ? value : limit; + var limit = LimitAtTheBound(antiderivative, x, bound, inside); + // A limit that is not decided is no value: the integral is left as written. Undefined would say it does not + // exist, and the integral of ln(x) erf(x) from 0 to 1 was NaN where it is -0.2613, for want of erf's limit at + // 0. https://github.com/asc-community/AngouriMath/issues/2081 + if (limit.Nodes.Any(node => node is Limitf)) + return null; + return limit.IsNaN ? value : limit; + } + + /// + /// The antiderivative's limit at a bound, remembered under the settings it was taken in, as + /// the cache of answers remembers an antiderivative. A definite integral left as written is evaluated + /// again by every candidate Simplify tries, and the limit that left it so is the slow part: at 0, + /// the antiderivative of ln(x) erf(x) is an infinity less an infinity that l'Hopital's rule does + /// not settle, two seconds of search each time, and the simplification ran past two minutes. + /// https://github.com/asc-community/AngouriMath/issues/2081 + /// + private static Entity LimitAtTheBound(Entity antiderivative, Variable x, Entity bound, ApproachFrom side) + { + if (limitsAtBounds is null || limitsAtBoundsUnder is null || !SettingsState.StillHolds(limitsAtBoundsUnder)) + { + limitsAtBounds = new(); + limitsAtBoundsUnder = SettingsState.Snapshot(); + } + var into = limitsAtBounds; + var stamp = limitsAtBoundsUnder; + var key = (antiderivative, x, bound, side); + if (into.TryGetValue(key, out var known)) + return known; + // Within a budget of the limits it asks along the way. Past it the limit is undecided and the integral left + // as written, which is no wrong answer. The most a decided limit at a bound took in the tests is 957; one + // spelling of the antiderivative of ln(x) Si(x) took over 38,000 in forty seconds without deciding. + var limit = Functions.Algebra.LimitFunctional.WithinABudget(LimitsAtABound, () => antiderivative.Limit(x, bound, side)); + if (SettingsState.StillHolds(stamp)) + { + if (into.Count >= 256) + into.Clear(); + into[key] = limit; + } + return limit; } + [System.ThreadStatic] private static Dictionary<(Entity, Variable, Entity, ApproachFrom), Entity>? limitsAtBounds; + /// How many limits the limit at one bound may ask in all. + private const int LimitsAtABound = 2000; + [System.ThreadStatic] private static object?[]? limitsAtBoundsUnder; /// /// Integrates numerically over between two bounds, without diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs index b00db583e..9fba3b24c 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs @@ -163,8 +163,38 @@ private static Entity ExpandLogarithm(Entity expr) _ => expr }; + /// + /// How many limits a budgeted computation may still take, every one it asks along the way counted, or + /// where there is no budget. Past it each is undecided. Set by . + /// + [System.ThreadStatic] private static int? remainingLimits; + + /// + /// with at most limits taken in all, for a caller that has a + /// use only for a limit it can have cheaply, and leaves the question open otherwise. + /// + internal static T WithinABudget(int limits, System.Func compute) + { + var enclosing = remainingLimits; + var start = enclosing is { } outer ? System.Math.Min(outer, limits) : limits; + remainingLimits = start; + try { return compute(); } + finally + { + // What was spent here is spent from the enclosing budget too. + var used = start - (remainingLimits ?? 0); + remainingLimits = enclosing is { } stillOuter ? stillOuter - used : null; + } + } + public static Entity? ComputeLimit(Entity expr, Variable x, Entity dest, ApproachFrom side = ApproachFrom.BothSides, bool acceptNaN = false) { + if (remainingLimits is { } remaining) + { + if (remaining <= 0) + return null; + remainingLimits = remaining - 1; + } // The approach is stated for the whole computation, not only where the destination // has been normalised to +oo. Everything below simplifies as it goes, and a rule that // may only fire while the approach is in scope is otherwise declining through most of diff --git a/Sources/Tests/UnitTests/Calculus/ImproperIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ImproperIntegralTest.cs index c5d83a9a2..77b7c4f39 100644 --- a/Sources/Tests/UnitTests/Calculus/ImproperIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/ImproperIntegralTest.cs @@ -59,6 +59,28 @@ public void ABoundWhereTheAntiderivativeIsUndefinedIsALimit(string integrand, st [Fact] public void AnAntiderivativeWithoutALimitLeavesNaN() => Assert.Equal(MathS.NaN, Definite("sin(x)", "0", "+oo")); + /// + /// And as written where the limit is not decided: undefined would say the integral does not exist, and + /// ln(x) erf(x) over [0, 1], whose antiderivative is an infinity less an infinity at 0 that no rule + /// settles, was NaN where it is -0.2613. The limit there is asked within a budget, and the simplification, + /// which asks it again for each spelling of the integrand it tries, ran past two minutes without one. + /// https://github.com/asc-community/AngouriMath/issues/2081 + /// + [Theory] + [InlineData("ln(x) * erf(x)")] + [InlineData("ln(x) * Si(x)")] + public void ABoundWhoseLimitIsNotDecidedLeavesTheIntegralAsWritten(string integrand) + { + Entity? result = null; + var task = System.Threading.Tasks.Task.Run(() => result = Definite(integrand, "0", "1")); + // The budget bounds the work, in limits; this wall is a backstop against a hang that the budget no longer + // bounds, not a measure of time. The erf integral takes about nine seconds alone, and a wall of sixty + // fired in the full suite on a loaded machine. + Assert.True(task.Wait(System.TimeSpan.FromMinutes(5)), $"the integral of {integrand} from 0 to 1 was still being simplified"); + Assert.IsType(result); + Assert.DoesNotContain(result!.Nodes, node => node == MathS.NaN); + } + /// A bound the substitution answers is answered as it was. [Theory] [InlineData("e^(-x)", "0", "+oo", "1")]