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