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)"); + } + } +}