From 7171eb06c340ae641e984e7de3297923bd54403e Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 4 Nov 2026 03:34:13 +0000 Subject: [PATCH] Take no undecided limit for an infinite one in l'Hopital's rule Whether a limit diverges was read off whether an infinity occurs in it, and an unevaluated limit(sin(u), x, +oo) holds one, as its destination. So l'Hopital's rule took sin(u) / cos(u), with neither part's limit taken, for oo/oo and differentiated it: tan(x^2 + x - 1) from the left at 0 answered -cos(-1) / sin(-1), 0.642, where the limit is tan(-1), -1.557. The same on 2.6.0. The rule, and the two places that build its quotients, now ask DivergesDecidedly, which turns an undecided limit away. The rewrites that ask IsInfiniteNode only whether they are worth trying keep it: f^g as e^(g ln f) is an equality whatever f does. Two answers had been reached through the mistake. The Stirling limits read -x + ln(2 pi x)/2 at +oo, asked from the left, unevaluated, as infinite -- which it is. An infinity has only one side and the one-sided path lacks the machinery the two-sided one has there, so a side asked for at a real infinity is now the two-sided limit; that also answers x - sqrt(x^2 + x) at +oo, -1/2, and e^x x at -oo, 0, both unevaluated before. And log(x, tanh(x))^(x^(pi + x)) at 0+ was 1 only because its base came back undecided; decided, it tends to 1 and the exponent to 0. A power whose base tends to a number other than 0 and whose exponent tends to 0 now answers 1. limitcheck, DEPTH=3, seeds 14, 16, 19, 23, 26 against master 37f73774b: the limit is another value 11 -> 6, each of the five now unevaluated, and one limit more answered. Found at SEED=26; no issue was filed. Co-Authored-By: Claude Opus 5.5 --- BREAKING-CHANGES.md | 25 +++++++ .../Limits/Solvers/Solvers.Definition.cs | 7 ++ .../Continuous/Limits/Transformations.cs | 39 ++++++++-- .../AnUndecidedLimitIsNotInfiniteTest.cs | 71 +++++++++++++++++++ 4 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/AnUndecidedLimitIsNotInfiniteTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index b3378a053..3d445b212 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -133,6 +133,31 @@ and some are left unevaluated ([#2267](https://github.com/asc-community/AngouriM | `"limitright(ln(sin(x)) / ln(cos(x)), x, 0)".ToEntity().Simplify()` | `-oo` | `+oo` | | `"limit(ln(x^2) / ln(cos(x)), x, 0)".ToEntity().Simplify()` | `-oo` | `+oo`, by [#2274](https://github.com/asc-community/AngouriMath/issues/2274) | +### A limit nobody could take is not read as infinite, and an infinity has one side + +**Silent: a quotient whose parts' limits were both left unevaluated is no longer answered by l'Hopital's rule, which +gave it a wrong value.** Whether a part diverges was read off whether an infinity occurs in its limit, and an unevaluated +`limit(sin(u), x, +oo)` holds one, as its destination. So `sin(u) / cos(u)`, with neither limit taken, was taken for +`oo / oo` and its derivatives' ratio answered `-cos(-1) / sin(-1)` for `tan(x^2 + x - 1)` from the left at 0, which is +0.642, where the limit is `tan(-1)`, -1.557. An unevaluated limit is now neither infinite nor finite. Some of these +limits are answered rightly, and some are left unevaluated. And a side asked for at `+oo` or `-oo`, which has only one, +is the limit there: it was taken by a path without the machinery for an infinity, and left unevaluated what it is. A +power whose base tends to a number other than 0 and whose exponent tends to 0 tends to 1, which is now answered +wherever the base's limit is found. One right answer of 2.6.0 is among those left unevaluated: `log(x^x, 3/2) - 1` from +the left at -1, which is `-1 - 0.129 i`, and which 2.7.0's previous changes had already turned into the wrong `-1`. + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"tan(x^2 + x - 1)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `-cos(-1) / sin(-1)` | `tan(-1)` | +| `"sin(x^2 + x - 1) / cos(x^2 + x - 1)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `-cos(-1) / sin(-1)` | `tan(-1)` | +| `"tan(x^2 - x - 1)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `-cos(-1) / sin(-1)` | `limitleft(tan(x ^ 2 - x - 1), x, 0)` | +| `"-x + ln(2*pi*x)/2".ToEntity().Limit("x", "+oo", ApproachFrom.Left)` | `limitleft(-x + ln(2 * pi * x) / 2, x, +oo)` | `-oo` | +| `"x - sqrt(x^2 + x)".ToEntity().Limit("x", "+oo", ApproachFrom.Left)` | `limitleft(x - sqrt(x ^ 2 + x), x, +oo)` | `-1/2` | +| `"e^x * x".ToEntity().Limit("x", "-oo", ApproachFrom.Right)` | `limitright(e ^ x * x, x, -oo)` | `0` | +| `"log(x, tanh(x)) ^ (x ^ (pi + x))".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `NaN`, in 10.7 s | `1` | +| `"tanh(log(x / (-1), 1/x))".ToEntity().Limit("x", -1, ApproachFrom.Right)` | `1` | unevaluated; the limit is -1.313 | +| `"log(x^x, 3/2) - 1".ToEntity().Limit("x", -1, ApproachFrom.Left)` | `ln(3/2) / ln(-1) - 1` | `limitleft(log(x ^ x, 3/2) - 1, x, -1)` | + ### A value put in for a derivative's variable is where it is evaluated **Silent: substituting for the variable of a derivative or an indefinite integral evaluates it there, where the value was diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs index 7efa0576b..42eb982b6 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs @@ -271,6 +271,13 @@ internal static T WithinABudget(int limits, System.Func compute) return null; remainingLimits = remaining - 1; } + // An infinity is approached from one side only, so a side asked for there is the + // limit itself -- and the two-sided path is where the machinery for an infinite + // destination lives. Taken one-sidedly, -x + ln(2 * pi * x) / 2 at +oo was left + // unevaluated where the limit is -oo. A complex infinity is not a real one and is + // turned away below as before. + if (side is not ApproachFrom.BothSides && dest is Real { IsFinite: false, IsNaN: false }) + side = ApproachFrom.BothSides; // 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/AngouriMath/Functions/Continuous/Limits/Transformations.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs index d84fdb3a6..6ca988618 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs @@ -517,6 +517,15 @@ private static bool IsRealAlong(Entity expr, Variable x) if (byStirling is null) { var baseLimit = EvalAssumingContinuous(@base.Limit(x, dest, side)); + // A base tending to a number other than 0 is not an indeterminate form, and + // its limit is the limit's power: g * ln(f) tends to 0 whatever the branch of + // ln(f), which stays bounded on the cut too, so f^g tends to 1. Answered here + // rather than left to the descent, which asks the base's limit by its own + // reading and can decline what this one has just taken: + // lim x->0+ log(x, tanh(x))^(x^(pi + x)) is 1, and was answered only while + // an undecided base was taken for an infinite one. + if (IsFiniteNode(baseLimit) && baseLimit.Evaled is Complex { IsZero: false } finite && finite.IsFinite) + return Integer.One; if (baseLimit != 0 && !IsInfiniteNode(baseLimit)) return null; // Every route out of ln(f) runs through differentiating f, so a base this @@ -789,6 +798,28 @@ private static bool IsInfiniteNode(Entity expr) => expr.ContainsNode("+oo") || expr.ContainsNode("-oo") || expr.Nodes.Any(node => node is Complex { IsFinite: false, IsNaN: false }); + /// + /// Whether a limit's value is infinite and was decided to be: , + /// but never for a value still holding a limit nobody could take. + /// + /// + /// That limit is a question rather than a value, and the destination it carries is not + /// what it tends to: limit(sin(u), x, +oo) holds a +oo and says nothing + /// about diverging. l'Hopital's rule read sin(u) / cos(u) with both limits undecided as + /// oo/oo, differentiated it, and answered lim x->0- tan(x^2 + x - 1) with + /// -cos(-1) / sin(-1), 0.642, where it is tan(-1), -1.557. The rule is only true of the + /// forms it names, so it asks this. + /// + /// is kept as it was for the rewrites that ask it whether + /// they are worth trying, which are equalities wherever they apply: f^g as e^(g ln f) + /// holds whatever f tends to, and an undecided base is exactly where it is worth asking. + /// + private static bool DivergesDecidedly(Entity expr) + => !HoldsAnUndecidedLimit(expr) && IsInfiniteNode(expr); + + private static bool HoldsAnUndecidedLimit(Entity expr) + => expr.Nodes.Any(node => node is Limitf); + /// /// Whether what the machinery handed back is an indeterminate form rather than a value: /// 0 / abs(0), (+oo * 0)^2 -- a shape assembled out of the parts' limits @@ -960,7 +991,7 @@ private static bool DivergesInMagnitude(Entity power, Variable x, Entity dest) } private static bool IsFiniteNode(Entity expr) - => !IsInfiniteNode(expr) && expr != MathS.NaN; + => !HoldsAnUndecidedLimit(expr) && !IsInfiniteNode(expr) && expr != MathS.NaN; /// /// What an inverse trigonometric function tends to where its argument grows without @@ -1400,7 +1431,7 @@ private static bool IsIndeterminateQuotient(Entity numerator, Entity denominator { var above = EvalAssumingContinuous(numerator.Limit(x, dest, side)); var below = EvalAssumingContinuous(denominator.Limit(x, dest, side)); - return above == 0 && below == 0 || IsInfiniteNode(above) && IsInfiniteNode(below); + return above == 0 && below == 0 || DivergesDecidedly(above) && DivergesDecidedly(below); } /// @@ -1433,7 +1464,7 @@ private static bool IsIndeterminateQuotient(Entity numerator, Entity denominator var limit = EvalAssumingContinuous(factor.Limit(x, dest, side)); if (vanishing is null && limit == 0) vanishing = factor; - else if (diverging is null && IsInfiniteNode(limit)) + else if (diverging is null && DivergesDecidedly(limit)) diverging = factor; else rest *= factor; @@ -1511,7 +1542,7 @@ bool DoesNotSettle(Entity argument) => argument.ContainsNode(x) if (expr is Divf(var num, var den)) if (EvalAssumingContinuous(num.Limit(x, dest, side)) is var numLimit && EvalAssumingContinuous(den.Limit(x, dest, side)) is var denLimit) if (numLimit == 0 && denLimit == 0 || - IsInfiniteNode(numLimit) && IsInfiniteNode(denLimit)) + DivergesDecidedly(numLimit) && DivergesDecidedly(denLimit)) if (num is not Number && den is not Number) if (num.ContainsNode(x) && den.ContainsNode(x) && !HasAStepThatJumpsNear(expr, x, dest, side)) diff --git a/Sources/Tests/UnitTests/Calculus/AnUndecidedLimitIsNotInfiniteTest.cs b/Sources/Tests/UnitTests/Calculus/AnUndecidedLimitIsNotInfiniteTest.cs new file mode 100644 index 000000000..bda5b38ba --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AnUndecidedLimitIsNotInfiniteTest.cs @@ -0,0 +1,71 @@ +// +// 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 Xunit; +using static AngouriMath.Entity; +using static AngouriMath.Entity.Number; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// l'Hopital's rule read a part whose limit nobody could take as infinite, because the unevaluated + /// limit(sin(u), x, +oo) holds a +oo -- its destination. sin(u) / cos(u) with both limits + /// undecided was taken for oo/oo and differentiated, and tan(x^2 + x - 1) at 0 from the left answered + /// -cos(-1) / sin(-1), which is -cot(-1) = 0.642, where the limit is tan(-1) = -1.557. The same on 2.6.0. + /// Found by limitcheck at DEPTH=3, SEED=26; no issue was filed. And a side asked for at an infinity + /// is now the limit there, which the Stirling limits were reading through the same mistake. + /// + [Trait("Area", "Calculus")] + public sealed class AnUndecidedLimitIsNotInfiniteTest + { + [Theory] + [InlineData("tan(x^2 + x - 1)", ApproachFrom.Left)] + [InlineData("sin(x^2 + x - 1) / cos(x^2 + x - 1)", ApproachFrom.Left)] + [InlineData("tan(x^2 + x - 1)", ApproachFrom.Right)] + public void IsTheTangent(string input, ApproachFrom side) + { + var limit = MathS.FromString(input).Limit("x", 0, side); + Assert.False(limit is Limitf, $"lim {side} x->0 {input} was left unevaluated"); + AssertIsTheTangent(input, side, limit); + } + + // These come back unevaluated: the parts' own limits from the left are not taken, and that is a decline, not a + // wrong answer. What they must not be is the quotient of the derivatives, as the first was before. + [Theory] + [InlineData("tan(x^2 - x - 1)", ApproachFrom.Left)] + [InlineData("tan(x^2 + x - 1)", ApproachFrom.BothSides)] + public void IsTheTangentOrUnevaluated(string input, ApproachFrom side) + { + var limit = MathS.FromString(input).Limit("x", 0, side); + if (limit is not Limitf) + AssertIsTheTangent(input, side, limit); + } + + // An infinity has one side, and asking for it one-sidedly was left unevaluated where the limit is a value. The + // Stirling limits relied on reading the first of these, unevaluated, as infinite: it is -oo, by luck. The last + // two are a power whose base tends to a number and whose exponent tends to 0. + [Theory] + [InlineData("-x + ln(2*pi*x)/2", "+oo", ApproachFrom.Left, "-oo")] + [InlineData("e^x * x", "-oo", ApproachFrom.Right, "0")] + [InlineData("x - sqrt(x^2 + x)", "+oo", ApproachFrom.Left, "-1/2")] + [InlineData("x - sqrt(x^2 + x)", "+oo", ApproachFrom.Right, "-1/2")] + // Answered only while its base, asked at +oo from the left, came back undecided and was taken for infinite. + // Decided, the base tends to 1 and the power to 0, which is 1 and not an indeterminate form. + [InlineData("log(x, tanh(x)) ^ (x ^ (pi + x))", "0", ApproachFrom.Right, "1")] + [InlineData("(x - 3) ^ x", "0", ApproachFrom.Left, "1")] + public void IsTheLimit(string input, string destination, ApproachFrom side, string expected) + => Assert.Equal(MathS.FromString(expected), MathS.FromString(input).Limit("x", destination, side)); + + private static void AssertIsTheTangent(string input, ApproachFrom side, Entity limit) + { + var difference = (limit - MathS.Tan(-1)).EvalNumerical(); + Assert.True(difference is Complex value && Complex.Abs(value) is Real abs && abs < 1e-10, + $"lim {side} x->0 {input} = {limit}, which is not tan(-1)"); + } + } +}