From 20cffb85eaac6e4ef36882d0c93f99a32e7c0f26 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 13 Nov 2026 08:40:32 +0000 Subject: [PATCH] Leave a two-sided limit unevaluated where a side stops at an indeterminate form A two-sided limit at a finite point compares its sides. When one came back NaN, the answer was NaN: "this does not exist". But the descent also returns NaN for an indeterminate form it could not resolve, such as 0 * floor(-oo) for x * floor(2/x) at 0, whose sides are 2 from each direction. That NaN means "undecided", and turning it into a two-sided NaN answered wrongly where the library had not decided anything. When the sides are asked outright, a side that stops at an indeterminate form with an infinity in it now counts as undecided, and the two-sided limit stays unevaluated. A side that settles as NaN still makes the two-sided limit NaN. So does an indeterminate form with only finite parts, such as 0/0 for (x - x)/(x - x), which does not exist anywhere near the point. Co-Authored-By: Claude Opus 5.5 --- BREAKING-CHANGES.md | 14 ++++++ .../Limits/Solvers/Solvers.Definition.cs | 46 +++++++++++++++---- ...ecidedSideLeavesTheLimitUnevaluatedTest.cs | 40 ++++++++++++++++ 3 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/AnUndecidedSideLeavesTheLimitUnevaluatedTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 05db67a7f..1090e0192 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -87,6 +87,20 @@ rational exponent is still written as its value. Found by limitcheck; no issue w | `"x^(abs(x)^arctan(2))".ToEntity().Limit("x", 0, ApproachFrom.Left)` | unevaluated | `1` | | `"x^(abs(x)^arctan(2))".ToEntity().Limit("x", 0, ApproachFrom.Right)` | unevaluated | `1` | +### A two-sided limit whose sides are undecided is left unevaluated + +**Silent: a two-sided limit is left unevaluated, rather than `NaN`, where a side's limit stops at an indeterminate form +with an infinity in it**, such as `0 * floor(-oo)`. That form is a side whose parts were never compared, not a side +without a limit, and `NaN` says the limit does not exist. Each limit below exists, and they are 2, 1, 1 +and 1 in order. 2.6.0 answered the third with a wrong `0`. Found by limitcheck; no issue was filed. + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"x * floor(2/x)".ToEntity().Limit("x", 0)` | `NaN` | `limit(x * floor(2 / x), x, 0)` | +| `"floor(1/x) * sin(x)".ToEntity().Limit("x", 0)` | `NaN` | `limit(floor(1 / x) * sin(x), x, 0)` | +| `"x^2 * floor(1/x^2)".ToEntity().Limit("x", 0)` | `0` | `limit(x ^ 2 * floor(1 / x ^ 2), x, 0)` | +| `"x * ceil(1/x)".ToEntity().Limit("x", 0)` | `NaN` | `limit(x * ceil(1 / x), x, 0)` | + ### A limit at the jump of a step function is not answered by substitution **Silent: a limit of an expression built on `floor`, `ceil`, `round` or `sgn`, where the step's argument arrives on one diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs index 66e0ec347..259d70522 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs @@ -180,24 +180,48 @@ node is Floorf or Ceilf or Roundf or Signumf /// #719, /// #596 /// - private static Entity? WhereBothSidesAgree(Entity expr, Variable x, Entity dest) + private static Entity? WhereBothSidesAgree(Entity expr, Variable x, Entity dest, out bool aSideIsUndecided) { + // Set where no answer came because a side has none yet, rather than because a side + // does not exist or the two differ. Neither side is then known to be NaN, so neither + // is the limit: x floor(2/x) at 0 is 2 from each side, the one-sided paths do not + // decide it, and the two-sided one answered NaN. + aSideIsUndecided = false; if (bothSidesDepth >= MaxBothSidesDepth || MathS.Settings.Codomain.Value is not AngouriMath.Core.Domain.Real && !IsRealOnBothSides(expr, x, dest)) return null; bothSidesDepth++; try { - if (ComputeLimit(expr, x, dest, ApproachFrom.Left) is not { } fromLeft - || fromLeft.Evaled == MathS.NaN) + var fromLeft = Decided(ComputeLimit(expr, x, dest, ApproachFrom.Left)); + if (fromLeft?.Evaled == MathS.NaN) return null; - if (ComputeLimit(expr, x, dest, ApproachFrom.Right) is not { } fromRight - || fromRight.Evaled == MathS.NaN) + // A side that does not exist settles the question whatever the other side does, + // so the right is still asked when the left is undecided. + var fromRight = Decided(ComputeLimit(expr, x, dest, ApproachFrom.Right)); + if (fromRight?.Evaled == MathS.NaN) return null; + if (fromLeft is null || fromRight is null) + { + aSideIsUndecided = true; + return null; + } return fromLeft == fromRight || ExpressionNumerical.AreEqual(fromLeft, fromRight) ? fromLeft : null; } finally { bothSidesDepth--; } + + // An indeterminate form a side's own limit could not read is NaN inside the machinery, + // where it stops the search, and withheld at the outermost call, where it is not an + // answer. One with an infinity in it, 0 * floor(-oo) for x floor(2/x), is a product or + // a quotient of parts whose rates were never compared, so that side is undecided rather + // than absent. One of finite parts only stays a side without a limit, as before: 0 / 0 + // for (x - x) / (x - x), which has no value anywhere, and ln(-2) / 0 for ln(-2) / ln(x) + // at 1, whose sides are infinities in different directions. + static Entity? Decided(Entity? limit) + => limit is { } && IsAnIndeterminateForm(limit) + && limit.Nodes.Any(static node => node is Real { IsFinite: false }) + ? null : limit; } /// @@ -719,9 +743,13 @@ node is Derivativef(var expression, Variable variable, var iterations) && iterat // again: tan(x/2) at pi, +oo and -oo, asked both sides through the whole of the // machinery each time, and a definite integral over [0, pi] spent its budget of // limits on that. - if ((fromLeft.Evaled == MathS.NaN || fromRight.Evaled == MathS.NaN) - && WhereBothSidesAgree(expr, x, dest) is { } agreed) - return agreed; + if (fromLeft.Evaled == MathS.NaN || fromRight.Evaled == MathS.NaN) + { + if (WhereBothSidesAgree(expr, x, dest, out var aSideIsUndecided) is { } agreed) + return agreed; + if (aSideIsUndecided) + return null; + } return MathS.NaN; // A two-sided limit cannot exist if the limit from left and right don't match. } else @@ -734,7 +762,7 @@ node is Derivativef(var expression, Variable variable, var iterations) && iterat // something that disagrees -- which is most of what the one-sided // fallbacks are for, so it is where the promotion is worth the most. // https://github.com/asc-community/AngouriMath/issues/719 - return WhereBothSidesAgree(expr, x, dest); + return WhereBothSidesAgree(expr, x, dest, out _); } } throw new AngouriBugException($"Unresolved enum parameter {side}"); diff --git a/Sources/Tests/UnitTests/Calculus/AnUndecidedSideLeavesTheLimitUnevaluatedTest.cs b/Sources/Tests/UnitTests/Calculus/AnUndecidedSideLeavesTheLimitUnevaluatedTest.cs new file mode 100644 index 000000000..b81941078 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AnUndecidedSideLeavesTheLimitUnevaluatedTest.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A two-sided limit whose sides stopped at an indeterminate form with an infinity in it, 0 * floor(-oo) for + /// x floor(2/x) at 0, answered NaN, which says there is no limit, where each side is 2. Such a side is undecided, + /// so the limit is left unevaluated. Found by limitcheck; no issue was filed. + /// + [Trait("Area", "Calculus")] + public sealed class AnUndecidedSideLeavesTheLimitUnevaluatedTest + { + [Theory] + [InlineData("x * floor(2/x)")] + [InlineData("floor(1/x) * sin(x)")] + [InlineData("x^2 * floor(1/x^2)")] + [InlineData("x * ceil(1/x)")] + public void IsNotNaN(string input) + => Assert.IsType(input.ToEntity().Limit("x", 0)); + + // A side that has no limit, or two sides that differ, still mean there is none: 0 / 0 everywhere, infinities + // in different directions, an oscillation, a jump. + [Theory] + [InlineData("(x - x) * sin(x) / ((x - x) * x)", "0")] + [InlineData("ln(-2) / ln(x)", "1")] + [InlineData("sin(1/x)", "0")] + [InlineData("floor(2/x)", "0")] + [InlineData("floor(x)", "0")] + public void IsStillNaN(string input, string destination) + => Assert.Equal(MathS.NaN, input.ToEntity().Limit("x", destination)); + } +}