diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index bbc9d7b7f..b421df3d7 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -338,6 +338,21 @@ Two-sided, each side is now taken with its own value, and `floor(x)^x` at 0 is 1 | `"floor(x)^tanh(x)".ToEntity().Limit("x", 0, ApproachFrom.Right)` | unevaluated | `0` | | `"floor(x)^x".ToEntity().Limit("x", 0)` | unevaluated | `NaN`, the sides differ | +### A function of a quotient tending to 0/0 has the value at the quotient's limit + +**Silent: a different answer.** A function of a quotient took the quotient's limit from the descent alone, which reads +0/0 or oo/oo off the two parts as NaN -- the claim that there is no limit -- where only the descent cannot tell. The +quotient's whole limit is asked now, once, through the remarkable limits and l'Hopital's rule, and only a value it +gives is taken. No issue was filed. + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `limit((sin(x)/x)^2, x, 0)` | `NaN` | `1` | +| `limit(exp(sin(x)/x), x, 0)` | `NaN` | `e` | +| `limit(sin(sin(x)/x), x, 0)` | `NaN` | `sin(1)` | +| `limit(exp((cos(x) - 1)/x^2), x, 0)` | `NaN` | `e ^ (-1/2)` | +| `limitright(exp(sin(x)/x), x, 0)` | unevaluated | `e` | + ### A two-sided limit of an expression real on both sides is where its sides agree **Silent: a two-sided limit that was `NaN` although both sides agree has their value, where the expression is real on diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs index ad93979a9..3adb73524 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs @@ -118,11 +118,55 @@ partial record Divf // which side the divisor vanishes from, and so comes back NaN. Only where // nothing above answered, and whatever it did answer is kept if this finds // nothing better. - return DivergesAtAVanishingDivisor(Dividend, Divisor, x, dist, side) ?? substituted; + if (DivergesAtAVanishingDivisor(Dividend, Divisor, x, dist, side) is { } diverges) + return diverges; + // An indeterminate form, 0/0 or oo/oo, read off the parts is NaN, which says there + // is no limit, where the descent only cannot tell: sin(x)/x at 0 is 1, which the + // whole limit reaches through the first remarkable limit, and a function of it -- + // (sin(x)/x)^2, exp(sin(x)/x), exp(ln(cos(x))/x^2) -- came out NaN, its argument + // asked only of the descent. The whole limit of the quotient is asked once, and + // only a value is taken, and as the number where the form it came back in holds the + // infinity it was taken at: -4/9 * 0 * (1 - (1 + (1/+oo)^2)^(1/3)), handed on as + // written, sent a definite integral astray. Folded, it is 0, and ln(2) * 2^(1/+oo) is + // ln(2), not its decimal. Where it has no + // value, what was answered is kept. + if (substituted?.Evaled == MathS.NaN && IsAnIndeterminateQuotient(dividend, divisor) + && AsTheWholeLimit(this, x, dist, side) is { } whole && whole.Evaled is Number { IsNaN: false } value) + return whole.InnerSimplified is var folded && folded.Nodes.Any(static node => node is Number { IsFinite: false }) + ? value : folded; + return substituted; } } } + /// Whether two parts' limits make a quotient 0/0 or oo/oo. + private static bool IsAnIndeterminateQuotient(Entity dividend, Entity divisor) + => (dividend.Evaled, divisor.Evaled) switch + { + (Complex { IsZero: true }, Complex { IsZero: true }) => true, + (Number { IsFinite: false, IsNaN: false }, Number { IsFinite: false, IsNaN: false }) => true, + _ => false, + }; + + /// The quotient whose whole limit is being asked from inside the descent, so that no other is asked inside it. + [System.ThreadStatic] private static HashSet? quotientsAskedWhole; + + private static Entity? AsTheWholeLimit(Entity quotient, Variable x, Entity dist, ApproachFrom side) + { + // One level: a whole limit asked from here descends again, and a retry inside it asked another, so that a + // definite integral's antiderivative at its bounds -- 1/((1 + x^2)^(1/3) (9 + x^2)) across 0 -- went from four + // seconds to past two minutes and was left unevaluated. + // Nor under a caller's budget of limits, which every limit asked here spends: fifty asked on the side of a + // definite integral's bounds left the bounds none, and the integral unevaluated. + if (Functions.Algebra.LimitFunctional.UnderALimitBudget) + return null; + quotientsAskedWhole ??= new(); + if (quotientsAskedWhole.Count > 0 || !quotientsAskedWhole.Add(quotient)) + return null; + try { return Functions.Algebra.LimitFunctional.ComputeLimit(quotient, x, dist, side); } + finally { quotientsAskedWhole.Remove(quotient); } + } + partial record Modf { internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs index 5b05a2891..7efa0576b 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs @@ -242,6 +242,9 @@ private static Entity ExpandLogarithm(Entity expr) /// [System.ThreadStatic] private static int? remainingLimits; + /// Whether a caller has set a budget of limits, which a limit asked on the side would spend. + internal static bool UnderALimitBudget => remainingLimits is not null; + /// /// 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. diff --git a/Sources/Tests/UnitTests/Calculus/AFunctionOfAnIndeterminateQuotientTest.cs b/Sources/Tests/UnitTests/Calculus/AFunctionOfAnIndeterminateQuotientTest.cs new file mode 100644 index 000000000..ca1d6d7f8 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AFunctionOfAnIndeterminateQuotientTest.cs @@ -0,0 +1,42 @@ +// +// 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; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A function of a quotient that tends to 0/0 took the quotient's limit from the descent alone, which reads 0/0 off + /// the two parts as NaN -- the claim that there is no limit -- where only the descent cannot tell: sin(x)/x at 0 + /// is 1, and (sin(x)/x)^2 and exp(sin(x)/x) were NaN two-sided and unevaluated from either side. The + /// quotient's whole limit is asked now, once, and only its value is taken. Found while checking a limit + /// limitcheck reported; no issue was filed. + /// + [Trait("Area", "Calculus")] + public sealed class AFunctionOfAnIndeterminateQuotientTest + { + [Theory] + [InlineData("(sin(x)/x)^2", ApproachFrom.BothSides, "1")] + [InlineData("exp(sin(x)/x)", ApproachFrom.BothSides, "e")] + [InlineData("exp(sin(x)/x)", ApproachFrom.Right, "e")] + [InlineData("sin(sin(x)/x)", ApproachFrom.BothSides, "sin(1)")] + [InlineData("exp((cos(x) - 1)/x^2)", ApproachFrom.BothSides, "e^(-1/2)")] + [InlineData("sqrt(sin(x)/x)", ApproachFrom.Right, "1")] + // As before. + [InlineData("sin(x)/x", ApproachFrom.BothSides, "1")] + [InlineData("exp(x/x)", ApproachFrom.BothSides, "e")] + public void HasTheValueOfTheFunctionAtTheQuotientsLimit(string input, ApproachFrom side, string expected) + { + var limit = MathS.FromString(input).Limit("x", 0, side); + Assert.IsAssignableFrom(limit.Evaled); + var difference = (limit - MathS.FromString(expected)).EvalNumerical(); + Assert.True(difference.Abs() < (Number.Real)1e-30, $"the limit of {input} at 0 is {limit}, where it is {expected}"); + } + } +}