diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs index 8980a0c27..3d49d7fb7 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs @@ -50,17 +50,57 @@ private static Entity ExpandLogarithm(Entity expr) return null; expr = expr.Replace(ExpandLogarithm); + // In front of both paths, not only the two-sided one. Each of these is guarded on + // a two-sided limit, and a two-sided limit that exists is also the limit from + // either side, so none of them says anything one-sided that it did not already say + // two-sided. Skipping them is what made (1 + x)^(1/x) at 0+ answer 1: without the + // second remarkable limit the descent reads it as 1^(+oo) and is definite about + // it, where the same expression approached from both sides gives e. + expr = expr.Replace(a => TrivialTrigonometricReplacement(a, x)); + expr = ApplyTrivialTransformations(expr, x, dest, (_, exprLim) => exprLim); + expr = ApplyFirstRemarkable(expr, x, dest); + expr = expr.Replace(c => ApplySecondRemarkable(c, x, dest)); + MultithreadingFunctional.ExitIfCancelled(); + if (side is ApproachFrom.Left or ApproachFrom.Right) - return expr.ComputeLimitDivideEtImpera(x, dest, side); + { + var oneSided = expr.ComputeLimitDivideEtImpera(x, dest, side); + if (oneSided is { } found && (acceptNaN || found.Evaled != MathS.NaN)) + return oneSided; + // The same rule the two-sided path falls through to, asked with the side it was + // given. l'Hopital's rule is stated one-sidedly to begin with -- the two-sided + // case is the two one-sided ones agreeing -- so this is not a weaker reading of + // it than the one above. Where the descent has nothing to say about a quotient, + // it is what answers (1 - cos(x)) / x^2 at 0+ with 1/2 rather than NaN, and + // csc(x) * x with 1: the csc rewrite leaves a product, which the descent does + // not take apart but which the rule reads back as the quotient x / sin(x). + if (ApplylHopitalRule(expr, x, dest, side) is { } lhopital && lhopital.Evaled != MathS.NaN) + return lhopital; + // The one-sided path is the only one with nothing behind it, and the descent + // can make an indeterminate form definite on the way down: for x * ln(x) at 0+ + // it substitutes the first factor's own limit and then asks for 0 * -oo, which + // is NaN. NaN is the claim that the limit does not exist, and here it is 0. + // + // Moving x out to infinity is what ComputeLimitImpl does for a finite + // destination in any case, so this asks the same question in the place where + // l'Hopital's rule and the rest of the machinery for infinity live -- none of + // which this path can otherwise reach. Only where nothing above answered, and + // whatever was found before is kept if this finds nothing better. + // Simplified, and not only for tidiness: the substitution leaves the + // destination behind as a term, and it is exactly that leftover which makes + // the answer NaN. x * ln(x) at 0+ becomes (0 + 1/x) * ln(0 + 1/x), whose limit + // at infinity comes back NaN, where the same expression written as + // (1/x) * ln(1/x) comes back 0. + if (dest.IsFinite + && ComputeLimit(expr.Substitute(x, side is ApproachFrom.Left ? dest - 1 / x : dest + 1 / x) + .InnerSimplified, + x, Real.PositiveInfinity) is { } byInfinity + && byInfinity.Evaled != MathS.NaN) + return byInfinity; + return oneSided; + } if (side is ApproachFrom.BothSides) { - expr = expr.Replace(a => TrivialTrigonometricReplacement(a, x)); - expr = ApplyTrivialTransformations(expr, x, dest, (_, exprLim) => exprLim); - expr = ApplyFirstRemarkable(expr, x, dest); - // expr = ApplySecondRemarkable(expr, x, dest); - expr = expr.Replace(c => ApplySecondRemarkable(c, x, dest)); - - MultithreadingFunctional.ExitIfCancelled(); if (!dest.IsFinite) { // just compute limit with no check for left/right equality diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs index b92023309..e59de5767 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs @@ -165,7 +165,18 @@ private static bool GrewTooMuch(Entity quotient, Entity applied) return reciprocal ? numerator / denominator : null; } - private static Entity? ApplylHopitalRule(Entity expr, Variable x, Entity dest) + /// + /// The side is carried through because the rule is stated one-sidedly to begin with -- + /// the two-sided case is the two one-sided ones agreeing -- so it is the same rule + /// either way, asked of the same quotient under the same premises. Carrying it is what + /// lets the rule answer where the two-sided reading has nothing to say, and the two + /// questions the rule asks along the way are both of that kind: what the parts tend to, + /// and what the differentiated quotient tends to. For (1 - cos(x)) / x^3 at 0 + /// the second of those reaches sin(x) / (3x^2), which is +oo on the right and + /// -oo on the left, so asking about both sides at once gives NaN and the step is + /// wasted. Asked one side at a time it answers. + /// + private static Entity? ApplylHopitalRule(Entity expr, Variable x, Entity dest, ApproachFrom side = ApproachFrom.BothSides) { if (lHopitalDepth == 0) lHopitalApplications = 0; @@ -175,16 +186,16 @@ private static bool GrewTooMuch(Entity quotient, Entity applied) // asking what the two parts tend to is itself a limit that the rule may be applied // to, and counting only the recursion left those two free to nest without a bound. lHopitalDepth++; - try { return ApplylHopitalRuleImpl(expr, x, dest); } + try { return ApplylHopitalRuleImpl(expr, x, dest, side); } finally { lHopitalDepth--; } } - private static Entity? ApplylHopitalRuleImpl(Entity expr, Variable x, Entity dest) + private static Entity? ApplylHopitalRuleImpl(Entity expr, Variable x, Entity dest, ApproachFrom side) { if (expr is not Divf && AsQuotient(expr) is { } quotient) expr = quotient; if (expr is Divf(var num, var den)) - if (EvalAssumingContinuous(num.Limit(x, dest)) is var numLimit && EvalAssumingContinuous(den.Limit(x, dest)) is var denLimit) + 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)) if (num is not Number && den is not Number) @@ -209,7 +220,7 @@ private static bool GrewTooMuch(Entity quotient, Entity applied) lHopitalChain!.Add(applied); try { - if (ComputeLimit(applied, x, dest) is { } resLim) + if (ComputeLimit(applied, x, dest, side) is { } resLim) return resLim; } finally { lHopitalChain.RemoveAt(lHopitalChain.Count - 1); } diff --git a/Sources/Tests/UnitTests/Calculus/OneSidedLimitTest.cs b/Sources/Tests/UnitTests/Calculus/OneSidedLimitTest.cs new file mode 100644 index 000000000..3cdfa1c7f --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/OneSidedLimitTest.cs @@ -0,0 +1,124 @@ +// +// Copyright (c) 2019-2022 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using AngouriMath; +using AngouriMath.Core; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A one-sided limit had nothing behind it. Where the two-sided and the infinite paths + /// fall through to l'Hopital's rule, this one returned whatever the descent produced -- + /// and the descent can make an indeterminate form definite on the way down, by putting a + /// part's own limit in place of the part. x * ln(x) at 0+ becomes 0 * ln(x), so 0 * -oo, + /// so NaN, and NaN is the claim that the limit does not exist. + /// + public sealed class OneSidedLimitTest + { + private static Entity Limit(string expression, string destination, ApproachFrom side) => + expression.ToEntity().Limit("x", destination.ToEntity(), side).Simplify(); + + private static void AssertLimit(string expression, string destination, ApproachFrom side, string expected) => + Assert.Equal(expected.ToEntity().Evaled, Limit(expression, destination, side).Evaled); + + /// + /// Every one of these came back NaN. sin(x) / x is the one worth naming: approached + /// from either side it is 1, and the library said the limit did not exist. + /// + [Theory] + [InlineData("sin(x) / x", "0", "1")] + [InlineData("tan(x) / x", "0", "1")] + [InlineData("x / sin(x)", "0", "1")] + [InlineData("x * ln(x)", "0", "0")] + public void FormsThatUsedToBeCalledNonExistentFromTheRight(string expression, string destination, string expected) => + AssertLimit(expression, destination, ApproachFrom.Right, expected); + + [Theory] + [InlineData("sin(x) / x", "0", "1")] + [InlineData("x / sin(x)", "0", "1")] + public void FormsThatUsedToBeCalledNonExistentFromTheLeft(string expression, string destination, string expected) => + AssertLimit(expression, destination, ApproachFrom.Left, expected); + + /// + /// The second remarkable limit was applied only on the two-sided path, so from one side + /// the descent read (1 + x)^(1/x) as 1^(+oo) and was definite about it: it answered 1, + /// where the same expression approached from both sides gives e. Not a NaN, so nothing + /// behind the descent would have been consulted -- this one had to be fixed in front + /// of it. + /// + [Theory] + [InlineData("(1 + x) ^ (1/x)", "0", ApproachFrom.Right, "e")] + [InlineData("(1 + x) ^ (1/x)", "0", ApproachFrom.Left, "e")] + [InlineData("(1 + 2 * x) ^ (1/x)", "0", ApproachFrom.Right, "e ^ 2")] + [InlineData("(1 + 2 * x) ^ (1/x)", "0", ApproachFrom.Left, "e ^ 2")] + public void TheRemarkableLimitsApplyFromOneSideToo(string expression, string destination, ApproachFrom side, string expected) => + AssertLimit(expression, destination, side, expected); + + /// + /// l'Hopital's rule was reached from the two-sided path only, so a one-sided limit of a + /// quotient the descent could not read had nothing to fall back on. csc(x) * x is the + /// odd one out: the csc rewrite makes it the product (1 / sin(x)) * x, which the descent + /// does not take apart, and the rule reads it back as the quotient x / sin(x). + /// + [Theory] + [InlineData("(1 - cos(x)) / x ^ 2", "0", ApproachFrom.Right, "1/2")] + [InlineData("(1 - cos(x)) / x ^ 2", "0", ApproachFrom.Left, "1/2")] + [InlineData("csc(x) * x", "0", ApproachFrom.Right, "1")] + [InlineData("csc(x) * x", "0", ApproachFrom.Left, "1")] + public void AQuotientTheDescentCannotReadGoesToTheRule(string expression, string destination, ApproachFrom side, string expected) => + AssertLimit(expression, destination, side, expected); + + /// + /// The rule is asked with the side it was given, which is what the rule is stated with + /// in the first place. Both questions it asks along the way -- what the two parts tend + /// to, and what the differentiated quotient tends to -- can have a one-sided answer and + /// no two-sided one. Here the first step of (1 - cos(x)) / x^3 reaches sin(x) / (3x^2), + /// which is +oo on the right and -oo on the left, so asking about both sides at once + /// gives NaN and the step is wasted. + /// + [Theory] + [InlineData("(1 - cos(x)) / x ^ 3", "0", ApproachFrom.Right, "+oo")] + [InlineData("(1 - cos(x)) / x ^ 3", "0", ApproachFrom.Left, "-oo")] + public void TheRuleIsAskedAboutTheSideItWasGiven(string expression, string destination, ApproachFrom side, string expected) => + AssertLimit(expression, destination, side, expected); + + /// + /// The one-sided answers that were already right, kept. The fallback is only consulted + /// where the descent returned nothing or NaN, so none of these should reach it at all. + /// + [Theory] + [InlineData("1 / x", "0", ApproachFrom.Right, "+oo")] + [InlineData("1 / x", "0", ApproachFrom.Left, "-oo")] + [InlineData("1 / x ^ 2", "0", ApproachFrom.Right, "+oo")] + [InlineData("1 / x ^ 2", "0", ApproachFrom.Left, "+oo")] + [InlineData("ln(x)", "0", ApproachFrom.Right, "-oo")] + [InlineData("ln(x) / x", "0", ApproachFrom.Right, "-oo")] + [InlineData("e ^ (1/x)", "0", ApproachFrom.Right, "+oo")] + [InlineData("e ^ (1/x)", "0", ApproachFrom.Left, "0")] + [InlineData("1 / (x - 2)", "2", ApproachFrom.Right, "+oo")] + [InlineData("1 / (x - 2)", "2", ApproachFrom.Left, "-oo")] + [InlineData("(x ^ 2 - 1) / (x - 1)", "1", ApproachFrom.Right, "2")] + [InlineData("(x ^ 2 - 1) / (x - 1)", "1", ApproachFrom.Left, "2")] + [InlineData("x / (x + 1)", "0", ApproachFrom.Right, "0")] + public void EstablishedOneSidedLimitsAreUnaffected(string expression, string destination, ApproachFrom side, string expected) => + AssertLimit(expression, destination, side, expected); + + /// + /// The fallback moves x out to infinity, which is what a finite destination does in any + /// case. It must not be reached for a destination that is already infinite, where the + /// substitution would mean nothing. + /// + [Theory] + [InlineData("1 / x", "+oo", ApproachFrom.Right, "0")] + [InlineData("x ^ 2 / (x ^ 2 + 1)", "+oo", ApproachFrom.Left, "1")] + [InlineData("1 / x", "-oo", ApproachFrom.Left, "0")] + public void AnInfiniteDestinationIsLeftAlone(string expression, string destination, ApproachFrom side, string expected) => + AssertLimit(expression, destination, side, expected); + } +}