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")]