Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 16 additions & 5 deletions Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
/// <remarks>
/// 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 <c>(1 - cos(x)) / x^3</c> at 0
/// the second of those reaches <c>sin(x) / (3x^2)</c>, 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.
/// </remarks>
private static Entity? ApplylHopitalRule(Entity expr, Variable x, Entity dest, ApproachFrom side = ApproachFrom.BothSides)
{
if (lHopitalDepth == 0)
lHopitalApplications = 0;
Expand All @@ -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)
Expand All @@ -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); }
Expand Down
124 changes: 124 additions & 0 deletions Sources/Tests/UnitTests/Calculus/OneSidedLimitTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
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);

/// <summary>
/// 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.
/// </summary>
[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);

/// <summary>
/// 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.
/// </summary>
[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);

/// <summary>
/// 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).
/// </summary>
[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);

/// <summary>
/// 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.
/// </summary>
[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);

/// <summary>
/// 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.
/// </summary>
[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);

/// <summary>
/// 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.
/// </summary>
[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);
}
}
Loading