From 1ea10ba7fc75b9c373a7662f649ed6fcb3e4a10b Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 13 Nov 2026 12:39:43 +0000 Subject: [PATCH] Answer a step function of an unbounded argument times a vanishing factor x floor(2/x) at 0 was left unevaluated, each side stopping at 0 * floor(+-oo), where it is 2. floor(g), ceil(g) and round(g) are within 1 of a real g, so f * step(g) - f * g tends to 0 wherever f does, and the two products have the same limit. A new solver at the end of the list read at +oo takes that limit from f * g, and only a value: where f * g has no limit it is left as it was. Co-Authored-By: Claude Opus 5.5 --- BREAKING-CHANGES.md | 25 ++++++++ .../Limits/Solvers/Limit.Solvers.cs | 41 +++++++++++++ .../Limits/Solvers/Solvers.Definition.cs | 11 +++- ...oundedArgumentTimesAVanishingFactorTest.cs | 61 +++++++++++++++++++ ...ecidedSideLeavesTheLimitUnevaluatedTest.cs | 7 +-- 5 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/AStepOfAnUnboundedArgumentTimesAVanishingFactorTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index a70ca6bd5..e0ee2fc71 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -42,6 +42,28 @@ A not-equal written `<>` is read as `not (a = b)`, and printed that way. ## Unreleased — since 2.6.0 +### A step function of an unbounded argument times a vanishing factor has the limit of the factor times the argument + +**Silent: a limit of `f * floor(g)`, `f * ceil(g)` or `f * round(g)`, where `f` tends to 0 and `g` is real, is +answered with the limit of `f * g`.** The step is within 1 of its argument, so the two products differ by `f` times +something bounded, which tends to 0. Each side used to stop at `0 * floor(+oo)`, which 2.6.0 answered with a wrong `0` +one-sided and `NaN` two-sided. Where `f * g` has no limit of its own the limit is left as it was. Found by limitcheck; +no issue was filed. + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"x * floor(2/x)".ToEntity().Limit("x", 0)` | `NaN` | `2` | +| `"x * floor(2/x)".ToEntity().Limit("x", 0, ApproachFrom.Right)` | `0` | `2` | +| `"floor(1/x) * sin(x)".ToEntity().Limit("x", 0)` | `NaN` | `1` | +| `"x^2 * floor(1/x^2)".ToEntity().Limit("x", 0)` | `0` | `1` | +| `"x * ceil(1/x)".ToEntity().Limit("x", 0)` | `NaN` | `1` | +| `"x * round(1/x)".ToEntity().Limit("x", 0)` | throws `OverflowException` | `1` | +| `"x^2 * floor(1/x)".ToEntity().Limit("x", 0)` | `NaN` | `0` | +| `"x * floor(1/x^2)".ToEntity().Limit("x", 0, ApproachFrom.Right)` | `0` | `+oo` | +| `"x * floor(1/x^2)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `0` | `-oo` | +| `"x * floor(2/x) + 3".ToEntity().Limit("x", 0)` | `limit(x * floor(2 / x) + 3, x, 0)` | `5` | +| `"floor(x) / x".ToEntity().Limit("x", "+oo")` | `0` | `1` | + ### A product with a quotient for a factor is read over that quotient's divisor in a one-sided limit **Silent: a one-sided limit of a product whose factors tend to 0 and to an infinity, one of them a quotient, is asked @@ -101,6 +123,9 @@ and 1 in order. 2.6.0 answered the third with a wrong `0`. Found by limitcheck; | `"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)` | +These four are answered with their values by the entry *A step function of an unbounded argument times a vanishing +factor has the limit of the factor times the argument*, above. + ### A two-sided limit whose side stops at a finite indeterminate form is left unevaluated **Silent: a two-sided limit is left unevaluated, rather than answered, where a side's limit stops at an indeterminate diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs index d3a77ec78..a84ff4bac 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs @@ -250,6 +250,47 @@ static bool OnAJump(Entity argument, System.Func jump) ? Real.NaN : null; + /// + /// A step function of an unbounded argument times a factor that vanishes: the limit of + /// f * floor(g) is the limit of f * g, and the same for ceil and + /// round. + /// + /// + /// floor(g) = g - r with 0 <= r < 1 wherever g is real, ceil(g) = g + r + /// with the same bound, and round(g) is within 1/2 of g. So + /// f * floor(g) - f * g is f times something bounded by 1, which tends to 0 + /// wherever f does, and the two products have the same limit, a value or an + /// infinity alike. Assumed, and checked: g is real-valued, and the other factors + /// tend to 0. Nothing is assumed of g's limit, which is the point -- the descent + /// puts each part's own limit in place of the part, so x floor(2/x) at 0 was read + /// as 0 * floor(+oo), a form it cannot settle, where it is 2 from either side. + /// + /// Only a limit is taken from it. A NaN for f * g would say the same of the + /// step, but inside the machinery NaN can also be a form nothing read, so it is left + /// to the rules that already decide those. + /// + internal static Entity? SolveAsAStepOfAnUnboundedArgument(Entity expr, Variable x) + { + if (expr is not (Mulf or Divf) || !expr.Nodes.Any(static node => node is Floorf or Ceilf or Roundf)) + return null; + var factors = Mulf.LinearChildren(expr); + for (var i = 0; i < factors.Count; i++) + { + if (factors[i] is not (Floorf or Ceilf or Roundf) || !factors[i].ContainsNode(x)) + continue; + var argument = factors[i].DirectChildren[0]; + if (!IsRealValued(argument, x)) + continue; + var rest = factors.Where((_, j) => j != i).Aggregate((Entity)Integer.One, static (product, factor) => product * factor); + if (!rest.ContainsNode(x) || !VanishesAtInfinity(rest, x)) + continue; + if (LimitFunctional.ComputeLimit((rest * argument).InnerSimplified, x, Real.PositiveInfinity) is { } limit + && limit.Evaled != MathS.NaN) + return limit; + } + return null; + } + private static bool VanishesAtInfinity(Entity expr, Variable x) => LimitFunctional.ComputeLimit(expr, x, Real.PositiveInfinity) is { } limit && limit.Evaled is Real { IsZero: true }; diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs index f3a9c027e..5d65376a6 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs @@ -130,6 +130,11 @@ private static Entity AsAPowerOfTheVariable(Entity node, Variable x) var oscillationResult = LimitSolvers.SolveAsOscillationWithoutLimit(expr, x); if (oscillationResult is { }) return oscillationResult; + // After both rules above, which settle the products they read without asking for + // another limit; this one asks for the limit of a second product. + var stepResult = LimitSolvers.SolveAsAStepOfAnUnboundedArgument(expr, x); + if (stepResult is { }) return stepResult; + return null; } @@ -184,8 +189,8 @@ node is Floorf or Ceilf or Roundf or Signumf { // 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. + // is the limit: floor(1/x) / floor(2/x) at 0 is 1/2 from each side, the one-sided + // paths do not decide it, and a NaN here would say there is no limit. aSideIsUndecided = false; if (bothSidesDepth >= MaxBothSidesDepth || MathS.Settings.Codomain.Value is not AngouriMath.Core.Domain.Real && !IsRealOnBothSides(expr, x, dest)) @@ -213,7 +218,7 @@ node is Floorf or Ceilf or Roundf or Signumf // 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 + // answer. One with an infinity in it, such as 0 * oo or oo / oo, is a product or // a quotient of parts whose rates were never compared, so that side is undecided rather // than absent. So is one of finite parts where the expression takes real values on that // side: erf(abs(x)) / x^4 at 0 stops at 0 / 0 from each side, is +oo from each, and was diff --git a/Sources/Tests/UnitTests/Calculus/AStepOfAnUnboundedArgumentTimesAVanishingFactorTest.cs b/Sources/Tests/UnitTests/Calculus/AStepOfAnUnboundedArgumentTimesAVanishingFactorTest.cs new file mode 100644 index 000000000..f4bbe7c71 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AStepOfAnUnboundedArgumentTimesAVanishingFactorTest.cs @@ -0,0 +1,61 @@ +// +// 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.Core; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// f floor(g), f ceil(g) and f round(g), where f tends to 0 and g is real, have the limit of f g: the step is within 1 + /// of its argument, so the two products differ by f times something bounded. x floor(2/x) at 0 was left unevaluated, + /// each side stopping at 0 * floor(+-oo), where it is 2. Each value here was checked at x = +-7/3 * 10^-k for k from 4 + /// to 8. No issue was filed. + /// + [Trait("Area", "Calculus")] + public sealed class AStepOfAnUnboundedArgumentTimesAVanishingFactorTest + { + [Theory] + [InlineData("x * floor(2/x)", "0", "2")] + [InlineData("floor(1/x) * sin(x)", "0", "1")] + [InlineData("x^2 * floor(1/x^2)", "0", "1")] + [InlineData("x * ceil(1/x)", "0", "1")] + [InlineData("x * round(1/x)", "0", "1")] + [InlineData("x * ceil(-2/x)", "0", "-2")] + [InlineData("-3 * x * floor(1/x)", "0", "-3")] + [InlineData("x^2 * floor(1/x)", "0", "0")] + [InlineData("(x - 1) * floor(1/(x - 1))", "1", "1")] + [InlineData("x * floor(2/x) + 3", "0", "5")] + [InlineData("floor(x) / x", "+oo", "1")] + public void FromEachSideAndBoth(string input, string destination, string expected) + { + var expr = input.ToEntity(); + Assert.Equal(expected.ToEntity(), expr.Limit("x", destination, ApproachFrom.Left).InnerSimplified); + Assert.Equal(expected.ToEntity(), expr.Limit("x", destination, ApproachFrom.Right).InnerSimplified); + Assert.Equal(expected.ToEntity(), expr.Limit("x", destination).InnerSimplified); + } + + // Where f g has no limit, neither has f floor(g): the sides diverge in different directions, so there is none. + [Theory] + [InlineData("x * floor(1/x^2)")] + [InlineData("sqrt(abs(x)) * floor(1/x)")] + public void SidesThatDivergeApart(string input) + { + var expr = input.ToEntity(); + Assert.Equal(Entity.Number.Real.NegativeInfinity, expr.Limit("x", 0, ApproachFrom.Left)); + Assert.Equal(Entity.Number.Real.PositiveInfinity, expr.Limit("x", 0, ApproachFrom.Right)); + Assert.Equal(MathS.NaN, expr.Limit("x", 0)); + } + + // x floor(sin(1/x)/x) is sin(1/x) up to a term that vanishes, and has no limit. The rule takes only a value from + // the limit of f g, so it leaves this unevaluated rather than claiming anything. + [Fact] + public void AnOscillatingProductIsNotAnswered() + => Assert.IsType("x * floor(sin(1/x)/x)".ToEntity().Limit("x", 0)); + } +} diff --git a/Sources/Tests/UnitTests/Calculus/AnUndecidedSideLeavesTheLimitUnevaluatedTest.cs b/Sources/Tests/UnitTests/Calculus/AnUndecidedSideLeavesTheLimitUnevaluatedTest.cs index 728948028..f6d4e37e4 100644 --- a/Sources/Tests/UnitTests/Calculus/AnUndecidedSideLeavesTheLimitUnevaluatedTest.cs +++ b/Sources/Tests/UnitTests/Calculus/AnUndecidedSideLeavesTheLimitUnevaluatedTest.cs @@ -20,10 +20,9 @@ namespace AngouriMath.Tests.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)")] + // 1/2 from each side. The four this was written for, x floor(2/x) among them, are answered now, and are pinned + // in AStepOfAnUnboundedArgumentTimesAVanishingFactorTest. + [InlineData("floor(1/x) / floor(2/x)")] // A side that stops at 0 / 0, or at c / 0, of an expression real beside the point is undecided too: // erf(abs(x)) / x^4 is +oo from each side, 1 / log(x^2, cos(x)) is +oo, abs(x)^erf(x) is 1. [InlineData("erf(abs(x)) / x^4")]