diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs index 0fd3881e5..6e2a33ec2 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs @@ -88,6 +88,8 @@ private static Entity ExpandLogarithm(Entity expr) // 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 (SolveAsIndeterminatePower(expr, x, dest, side) is { } byExponent) + return byExponent; 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 @@ -127,6 +129,8 @@ private static Entity ExpandLogarithm(Entity expr) // were left with nothing to catch them. The rule is only allowed to improve // on what is already there: sqrt(x) / sqrt(x + 1) merely turns into its own // reciprocal, and a NaN from it would claim the limit does not exist. + if (SolveAsIndeterminatePower(expr, x, dest, ApproachFrom.Left) is { } byExponent) + return byExponent; if (ApplylHopitalRule(expr, x, dest) is { } lhopital && lhopital.Evaled != MathS.NaN) return lhopital; // The rewrites are worth their cost only where there is no answer without diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs index 8ce803ff5..667aed301 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs @@ -52,6 +52,65 @@ private static Entity ApplySecondRemarkable(Entity expr, Variable x, Entity dest _ => expr }; + /// + /// The limit of f(x)^g(x) where the pair is indeterminate and the second remarkable + /// limit does not already cover it -- that is, 0^0 and oo^0 -- or + /// where that is not the shape or the exponent settles nothing. + /// + /// + /// The descent substitutes each part's own limit, so both of these arrive as 0^0, which + /// is NaN. Written over as e^(g * ln f), the same question is the limit of a product of + /// something vanishing with something diverging, which the rules below can take apart. + /// + /// The exponent is asked as a limit of its own rather than rewritten in place, because a + /// rewrite would only hand the descent a product it reads no better than the power: the + /// descent substitutes the parts' limits and does not apply l'Hopital's rule to a part. + /// Asking outright is what puts the whole machinery behind the exponent. + /// + /// 1^oo is left to , which answers it more directly, + /// and 0^oo and oo^oo are not indeterminate at all. + /// + private static Entity? SolveAsIndeterminatePower(Entity expr, Variable x, Entity dest, ApproachFrom side) + { + if (expr is not Powf(var @base, var power) + || !@base.ContainsNode(x) || !power.ContainsNode(x) + || indeterminatePowerDepth >= MaxIndeterminatePowerDepth) + return null; + if (EvalAssumingContinuous(power.Limit(x, dest, side)) != 0) + return null; + var baseLimit = EvalAssumingContinuous(@base.Limit(x, dest, side)); + if (baseLimit != 0 && !IsInfiniteNode(baseLimit)) + return null; + // Every route out of ln(f) runs through differentiating f, so a base this library + // cannot differentiate is one the rewrite cannot finish on: it would only hand the + // rules an expression with a hole in it and let them work at it. A factorial is the + // case that matters -- its derivative wants the digamma function, which is not here, + // and comes back as NaN -- and lim x->+oo ((x!) / x^x)^(1/x) is the expression. It + // has no answer either way, and without this it takes a long time not to find one. + var derivative = @base.Differentiate(x).InnerSimplified; + if (derivative.Nodes.Any(node => node is Derivativef || node == MathS.NaN)) + return null; + + indeterminatePowerDepth++; + try + { + if (ComputeLimit((power * MathS.Ln(@base)).InnerSimplified, x, dest, side) is not { } exponent + || exponent.Evaled == MathS.NaN) + return null; + return MathS.e.Pow(exponent).InnerSimplified; + } + finally { indeterminatePowerDepth--; } + } + + /// + /// How deep the rewriting of one power into another may go. The exponent it asks about + /// is a limit in its own right and may hold a power of the same shape, so without a + /// bound the work would multiply. + /// + private const int MaxIndeterminatePowerDepth = 3; + + [ThreadStatic] private static int indeterminatePowerDepth; + private static bool IsInfiniteNode(Entity expr) => expr.ContainsNode("+oo") || expr.ContainsNode("-oo"); // TODO: is it correct? @@ -248,12 +307,22 @@ private static (Entity Numerator, Entity Denominator) SplitProduct(Entity expr) /// (sin(x) - x) / (x * sin(x)), which is 0/0 and which the rule settles at 0 in /// three steps. /// - private static Entity? AsQuotient(Entity expr, Variable x) + private static Entity? AsQuotient(Entity expr, Variable x, Entity dest, ApproachFrom side) { if (expr is Mulf) { + // Taking the reciprocal factors out is the better reading where it gives the + // rule something it can use -- x * e^(-x) comes out as the clean x / e^x -- so + // it is tried first. But it can also hide the indeterminacy rather than expose + // it: tan(x) * ln(x) has been written sin(x) / cos(x) * ln(x) by the time it + // arrives, and splitting on the reciprocal gives sin(x) * ln(x) / cos(x), whose + // divisor tends to 1. That is no longer a quotient the rule reads, so the other + // arrangement is tried in its place rather than after it. var (numerator, denominator) = SplitProduct(expr); - return denominator == 1 ? null : numerator / denominator; + if (denominator != 1 && IsIndeterminateQuotient(numerator, denominator, x, dest, side)) + return numerator / denominator; + return AsQuotientOfVanishingAndDiverging(expr, x, dest, side) + ?? (denominator == 1 ? null : numerator / denominator); } if (expr is not (Sumf or Minusf)) return null; @@ -277,6 +346,56 @@ private static (Entity Numerator, Entity Denominator) SplitProduct(Entity expr) return worthIt && combined is { } && common is { } ? (combined / common).InnerSimplified : null; } + /// + /// Whether a quotient is one of the two forms l'Hopital's rule reads: 0/0 or oo/oo. + /// + private static bool IsIndeterminateQuotient(Entity numerator, Entity denominator, Variable x, Entity dest, ApproachFrom side) + { + 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); + } + + /// + /// A product of something vanishing with something diverging, written as the quotient + /// of the diverging factor by the reciprocal of the vanishing one, or + /// where it is not that shape. + /// + /// + /// This is the other way a product can be indeterminate without being written as a + /// quotient, and the split above cannot see it: sin(x) * ln(x) has no reciprocal + /// factor at all, so both halves go into the numerator and it comes back unchanged. + /// + /// The diverging factor goes on top and the vanishing one is inverted underneath, not + /// the other way round, though both give an indeterminate quotient. Differentiating + /// ln(x) / csc(x) gets rid of the logarithm and arrives at an answer; the other + /// arrangement, sin(x) / (1 / ln(x)), differentiates into a product of the same + /// shape as the one it started from and goes round. + /// + private static Entity? AsQuotientOfVanishingAndDiverging(Entity expr, Variable x, Entity dest, ApproachFrom side) + { + Entity? vanishing = null, diverging = null; + Entity rest = 1; + foreach (var factor in Mulf.LinearChildren(expr)) + { + if (!factor.ContainsNode(x)) + { + rest *= factor; + continue; + } + var limit = EvalAssumingContinuous(factor.Limit(x, dest, side)); + if (vanishing is null && limit == 0) + vanishing = factor; + else if (diverging is null && IsInfiniteNode(limit)) + diverging = factor; + else + rest *= factor; + } + return vanishing is { } zero && diverging is { } infinity + ? rest * infinity / (1 / zero).InnerSimplified + : null; + } + /// /// 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 @@ -304,7 +423,7 @@ private static (Entity Numerator, Entity Denominator) SplitProduct(Entity expr) private static Entity? ApplylHopitalRuleImpl(Entity expr, Variable x, Entity dest, ApproachFrom side) { - if (expr is not Divf && AsQuotient(expr, x) is { } quotient) + if (expr is not Divf && AsQuotient(expr, x, dest, side) is { } quotient) expr = quotient; 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) diff --git a/Sources/Tests/UnitTests/Calculus/IndeterminatePowerTest.cs b/Sources/Tests/UnitTests/Calculus/IndeterminatePowerTest.cs new file mode 100644 index 000000000..0c8d0ea70 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/IndeterminatePowerTest.cs @@ -0,0 +1,115 @@ +// +// 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 +{ + /// + /// The indeterminate forms that are not quotients. Of the three powers, only 1^oo had a + /// rule -- the second remarkable limit -- so 0^0 and oo^0 arrived at the descent, which + /// substitutes each part's own limit and hands back 0^0, that is NaN. And a product of + /// something vanishing with something diverging had no reading either unless one of the + /// factors happened to be written as a reciprocal. + /// + public sealed class IndeterminatePowerTest + { + 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); + + /// + /// 0^0. Over to the exponent, each of these is the limit of g * ln(f), which the rules + /// for a vanishing-against-diverging product can take apart. + /// + [Theory] + [InlineData("x ^ x", "0", ApproachFrom.Right, "1")] + [InlineData("x ^ sin(x)", "0", ApproachFrom.Right, "1")] + [InlineData("sin(x) ^ x", "0", ApproachFrom.Right, "1")] + [InlineData("(1/x) ^ (1/x)", "+oo", ApproachFrom.Left, "1")] + public void ZeroToTheZero(string expression, string destination, ApproachFrom side, string expected) => + AssertLimit(expression, destination, side, expected); + + /// + /// And 0^0 is not always 1, which is the whole reason it is indeterminate: the exponent + /// decides, and here it decides on e. + /// + [Theory] + [InlineData("x ^ (1 / ln(x))", "0", ApproachFrom.Right, "e")] + [InlineData("x ^ (2 / ln(x))", "0", ApproachFrom.Right, "e ^ 2")] + public void ZeroToTheZeroIsNotAlwaysOne(string expression, string destination, ApproachFrom side, string expected) => + AssertLimit(expression, destination, side, expected); + + [Theory] + [InlineData("x ^ (1/x)", "+oo", ApproachFrom.Left, "1")] + [InlineData("x ^ (1 / ln(x))", "+oo", ApproachFrom.Left, "e")] + [InlineData("(1/x) ^ x", "0", ApproachFrom.Right, "1")] + public void InfinityToTheZero(string expression, string destination, ApproachFrom side, string expected) => + AssertLimit(expression, destination, side, expected); + + /// + /// A product of something vanishing with something diverging, written as the diverging + /// factor over the reciprocal of the vanishing one. sin(x) * ln(x) is the one worth + /// naming: it has no reciprocal factor at all, so the split that handles x * e^(-x) sees + /// nothing in it. + /// + [Theory] + [InlineData("sin(x) * ln(x)", "0", ApproachFrom.Right, "0")] + [InlineData("x * ln(x)", "0", ApproachFrom.Right, "0")] + [InlineData("sqrt(x) * ln(x)", "0", ApproachFrom.Right, "0")] + [InlineData("x * cotan(x)", "0", ApproachFrom.Right, "1")] + [InlineData("(1 - cos(x)) * cotan(x)", "0", ApproachFrom.Right, "0")] + public void AVanishingFactorAgainstADivergingOne(string expression, string destination, ApproachFrom side, string expected) => + AssertLimit(expression, destination, side, expected); + + /// + /// The forms that already had a reading must keep it. 1^oo belongs to the second + /// remarkable limit, which answers it more directly than going through the exponent + /// would; x * e^(-x) is read by taking the reciprocal factor out, which gives the + /// tidier x / e^x than inverting the other half would; and a base that tends to + /// anything else raised to a vanishing power is not indeterminate at all. + /// + [Theory] + [InlineData("(1 + x) ^ (1/x)", "0", ApproachFrom.Right, "e")] + [InlineData("(1 + 2 * x) ^ (1/x)", "0", ApproachFrom.Right, "e ^ 2")] + [InlineData("(1 + 1/x) ^ x", "+oo", ApproachFrom.Left, "e")] + [InlineData("x * e ^ (-x)", "+oo", ApproachFrom.Left, "0")] + [InlineData("x ^ 4 * e ^ (-x)", "+oo", ApproachFrom.Left, "0")] + [InlineData("(2 + x) ^ (1/x)", "0", ApproachFrom.Right, "+oo")] + [InlineData("x ^ x", "0", ApproachFrom.Left, "1")] + [InlineData("ln(x) / x", "+oo", ApproachFrom.Left, "0")] + public void EstablishedLimitsAreUnaffected(string expression, string destination, ApproachFrom side, string expected) => + AssertLimit(expression, destination, side, expected); + + /// + /// At an infinite destination there is only one direction to come from, so these need no + /// side of their own. + /// + [Theory] + [InlineData("x ^ (1 / ln(x))", "+oo", "e")] + [InlineData("x ^ (1/x)", "+oo", "1")] + public void TheFormsAtInfinityNeedNoSide(string expression, string destination, string expected) => + Assert.Equal(expected.ToEntity().Evaled, + expression.ToEntity().Limit("x", destination.ToEntity()).Simplify().Evaled); + + /// + /// x^x at 0 is left with no two-sided limit, and that is right rather than a gap: x^x is + /// not real for negative x, so there is no left-hand limit to agree with the right-hand + /// one. The 1 that comes back from the left is the complex continuation, not a real + /// limit, and the suite has pinned the two-sided answer as non-existent all along. + /// + [Fact] + public void APowerThatIsNotRealOnOneSideHasNoTwoSidedLimit() => + Assert.Equal(MathS.NaN.Evaled, + "x ^ x".ToEntity().Limit("x", 0).Simplify().Evaled); + } +}