From fc4622b87150136b5864c6e3eec1ded93f51d6ff Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 23 Oct 2026 19:17:56 +0000 Subject: [PATCH 1/2] Leave a definite integral as written where a bound's limit is not decided 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. The integral of ln(x) erf(x) over [0, 1] was NaN where it is -0.2613, and of ln(x) Si(x) where it is -0.2466. It is left as written now. Left as written, Simplify evaluates it again for each spelling of the integrand it tries, and one spelling of ln(x) Si(x)'s antiderivative at 0 sent the limit engine through more than 38,000 nested limits in forty seconds without deciding. So the limit at a bound is asked within a budget of 2,000 limits taken along the way, past which it is undecided, and each is remembered under the settings it was taken in. The most a decided limit at a bound took in the tests is 957. The two simplifications end in 2.2 and 7 s. Fixes #2081. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 13 +++++ .../Integration/Integration.Definition.cs | 53 +++++++++++++++++-- .../Limits/Solvers/Solvers.Definition.cs | 30 +++++++++++ .../Calculus/ImproperIntegralTest.cs | 19 +++++++ 4 files changed, 110 insertions(+), 5 deletions(-) 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..08045ebbd 100644 --- a/Sources/Tests/UnitTests/Calculus/ImproperIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/ImproperIntegralTest.cs @@ -59,6 +59,25 @@ 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")); + Assert.True(task.Wait(System.TimeSpan.FromSeconds(60)), $"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")] From 93e4df901922a92054d61ba40c797b704588b529 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 23 Oct 2026 22:34:39 +0000 Subject: [PATCH 2/2] Give the undecided-bound test a wall that cannot fire The limit budget bounds the work; the sixty-second wall measured the machine, and fired in the full suite on a loaded one, where the erf integral takes about nine seconds alone. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- Sources/Tests/UnitTests/Calculus/ImproperIntegralTest.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/Tests/UnitTests/Calculus/ImproperIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ImproperIntegralTest.cs index 08045ebbd..77b7c4f39 100644 --- a/Sources/Tests/UnitTests/Calculus/ImproperIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/ImproperIntegralTest.cs @@ -73,7 +73,10 @@ public void ABoundWhoseLimitIsNotDecidedLeavesTheIntegralAsWritten(string integr { Entity? result = null; var task = System.Threading.Tasks.Task.Run(() => result = Definite(integrand, "0", "1")); - Assert.True(task.Wait(System.TimeSpan.FromSeconds(60)), $"the integral of {integrand} from 0 to 1 was still being simplified"); + // 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); }