Skip to content

Commit fa87f01

Browse files
Answer the limits that come out as infinity minus infinity (#682)
lim x -> +oo of e^x - x, of x - ln(x) and of sqrt(x^2 + x) - x were all left unevaluated. Every solver reads either a polynomial or a substitution of +oo into the expression, and a difference of two parts that each grow without bound is neither: substituting gives oo - oo, which says nothing. Two readings settle most of them. Where one part outgrows the other the answer is that part's, which is what the ratio of the two says -- x / e^x tends to 0, so e^x - x tends to +oo. Where the two grow alike the ratio tends to 1 and says nothing, and then a - b = (a^2 - b^2) / (a + b), an identity wherever a + b is not zero, which on the way to +oo it is not. It is worth using where squaring removes a root: the leading terms of the numerator cancel and what is left is an ordinary quotient, so sqrt(x^2 + x) - x becomes x / (sqrt(x^2 + x) + x). Neither reading works on a root of a polynomial, because no solver can say anything about one. sqrt(x^2 + x) is now written x * sqrt(1 + 1/x), which puts its growth in plain sight and leaves a factor that +oo can simply be substituted into. That is sound in the one direction this is used in: P = x^d * (P / x^d), and (uv)^r = u^r v^r wants u positive, which x^d is on the way to +oo, and a destination of -oo has already been turned into +oo by substituting -x for x. It also answers sqrt(x^2 + x) / x, x / sqrt(x^2 + 1) and sqrt(x^2 - x) / x, none of which had an answer before. An odd degree is not covered: the growth of sqrt(x^3 + x) is x^(3/2), and l'Hopital's rule is stopped from working on a quotient containing that. All of this is reached only once the expression as written has been found to have no answer, so nothing that had one is asked twice, and it sits beside l'Hopital's rule rather than in the descent that visits every part of every expression -- putting it there instead cost the test suite four times its running time. Simplification is asked again as well, since it can hand back an expression of another kind altogether and which parts a limit is broken into is decided by that kind: sqrt(x^2 + 1) / sqrt(x^2 + 3x) is broken up as a quotient, while what simplifying gives is the single root sqrt((x^2 + 1) / (x^2 + 3x)), whose argument can be read straight off. That one took 2.2 seconds to reach no answer and now takes 14 ms to reach 1. 3937 tests, corpus 93/117 from 91 with nothing wrong, in error or timing out, and the oo - oo family complete. 18 of the 27 new tests fail without the change; the 9 that pass are the ones pinning what already worked.
1 parent 47ba93b commit fa87f01

4 files changed

Lines changed: 296 additions & 11 deletions

File tree

‎Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ private static Entity ExpandLogarithm(Entity expr)
7575
// reciprocal, and a NaN from it would claim the limit does not exist.
7676
if (ApplylHopitalRule(expr, x, dest) is { } lhopital && lhopital.Evaled != MathS.NaN)
7777
return lhopital;
78+
// The rewrites are worth their cost only where there is no answer without
79+
// them, and each of them costs an expansion or a simplification of the
80+
// whole expression. The descent visits every part of every expression, so
81+
// rather than pay for them at each of those parts on every limit ever
82+
// taken, the descent is walked a second time with them turned on, and only
83+
// for an expression that has just been found to have no answer at all.
84+
if (SolveByRewriting(expr, x, dest) is { } rewritten && rewritten.Evaled != MathS.NaN)
85+
return rewritten;
7886
return atInfinity;
7987
}
8088
else if (expr.ComputeLimitDivideEtImpera(x, dest, ApproachFrom.Left) is { } fromLeft

‎Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs‎

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
//
77

88
using AngouriMath.Core.Multithreading;
9+
using PeterO.Numbers;
910
using System;
1011
using System.Linq;
1112
using static AngouriMath.Entity;
13+
using static AngouriMath.Entity.Number;
1214

1315
namespace AngouriMath.Functions.Algebra
1416
{
@@ -129,6 +131,8 @@ private static bool AlreadyBeingDifferentiated(Entity quotient)
129131
private static bool GrewTooMuch(Entity quotient, Entity applied)
130132
=> applied.Nodes.Count() > quotient.Nodes.Count() + 8;
131133

134+
[ThreadStatic] private static bool suppresslHopital;
135+
132136
/// <summary>
133137
/// A product that has a reciprocal factor in it, rewritten as a quotient, or
134138
/// <see langword="null"/> if it has none. The rule below only reads quotients, so
@@ -169,7 +173,7 @@ private static bool GrewTooMuch(Entity quotient, Entity applied)
169173
{
170174
if (lHopitalDepth == 0)
171175
lHopitalApplications = 0;
172-
if (lHopitalDepth >= MaxlHopitalDepth || lHopitalApplications >= MaxlHopitalApplications)
176+
if (lHopitalDepth >= MaxlHopitalDepth || lHopitalApplications >= MaxlHopitalApplications || suppresslHopital)
173177
return null;
174178
// Held for the whole of the rule and not just for the recursive call below, since
175179
// asking what the two parts tend to is itself a limit that the rule may be applied
@@ -217,6 +221,184 @@ private static bool GrewTooMuch(Entity quotient, Entity applied)
217221
return null;
218222
}
219223

224+
/// <summary>
225+
/// How deep one limit may go into rewriting itself. Each of the rewrites below hands
226+
/// back another limit to take, and that one is entitled to be rewritten in turn, so
227+
/// without a bound the work would multiply.
228+
/// </summary>
229+
private const int MaxRewriteDepth = 2;
230+
231+
[ThreadStatic] private static int rewriteDepth;
232+
233+
/// <summary>
234+
/// The readings that need the expression rewritten before any solver can see anything
235+
/// in it. Every one of them costs an expansion or a simplification of the whole
236+
/// expression, which is why this sits here rather than in the descent that visits each
237+
/// of its parts, and why it is reached only once it is settled that the expression as
238+
/// written has no answer.
239+
/// </summary>
240+
private static Entity? SolveByRewriting(Entity expr, Variable x, Entity dest)
241+
{
242+
if (rewriteDepth >= MaxRewriteDepth)
243+
return null;
244+
// -oo is +oo with -x written for x, the same substitution the solvers are handed.
245+
// Reading the growth of a root off x^d depends on it: x^d is positive in the one
246+
// direction only.
247+
if (dest.Evaled is Real { IsNegative: true })
248+
expr = expr.Substitute(x, -x);
249+
var toInfinity = Real.PositiveInfinity;
250+
var simplified = expr.Simplify();
251+
if (simplified is Providedf(var body, _))
252+
simplified = body;
253+
254+
rewriteDepth++;
255+
try
256+
{
257+
// Simplification can hand back an expression of another kind altogether, and
258+
// which parts a limit is broken into is decided by that kind:
259+
// sqrt(x^2 + 1) / sqrt(x^2 + 3x) is broken up as a quotient, while what
260+
// simplifying gives is the single root sqrt((x^2 + 1) / (x^2 + 3x)), whose
261+
// argument can be read straight off.
262+
if (simplified.GetType() != expr.GetType()
263+
&& Settled(simplified.ComputeLimitDivideEtImpera(x, toInfinity, ApproachFrom.Left)) is { } byShape)
264+
return byShape;
265+
266+
if (ExtractRadicalGrowth(simplified, x) is { } extracted
267+
&& Settled(extracted.ComputeLimitDivideEtImpera(x, toInfinity, ApproachFrom.Left)) is { } byGrowth)
268+
return byGrowth;
269+
270+
return Settled(SolveAsDifferenceOfInfinities(simplified, x));
271+
}
272+
finally { rewriteDepth--; }
273+
274+
// Rewriting brings domain conditions with it -- dividing by x^d is only the same
275+
// expression where x is not zero -- and a limit is taken of a continuous
276+
// expression regardless of the points where it is undefined, as the solvers
277+
// themselves do with the same shape.
278+
static Entity? Settled(Entity? limit)
279+
{
280+
while (limit is Providedf(var inner, _)) limit = inner;
281+
return limit is null || limit.Evaled == MathS.NaN ? null : limit;
282+
}
283+
}
284+
285+
/// <summary>
286+
/// The whole expression with every root of a polynomial rewritten so that its growth is
287+
/// a factor of its own -- <c>sqrt(x^2 + x)</c> becomes <c>x * sqrt(1 + 1/x)</c> -- or
288+
/// <see langword="null"/> if it has no such root. Every solver reads a polynomial or a
289+
/// substitution of +oo, and neither can say anything about a root of a sum, so
290+
/// <c>lim x -&gt; +oo sqrt(x^2 + x) / x</c> was left unevaluated while the rewritten
291+
/// <c>sqrt(1 + 1/x)</c> is settled by substitution alone.
292+
/// </summary>
293+
/// <remarks>
294+
/// <c>P = x^d * (P / x^d)</c>, and <c>(uv)^r = u^r v^r</c> needs <c>u</c> to be positive,
295+
/// which <c>x^d</c> is for every x past some point on the way to +oo. That is the only
296+
/// direction this is used in: a destination of -oo has already been turned into +oo by
297+
/// substituting -x for x before any of this runs.
298+
/// </remarks>
299+
private static Entity? ExtractRadicalGrowth(Entity expr, Variable x)
300+
{
301+
if (!expr.Nodes.Any(IsRootOfASum))
302+
return null;
303+
var extracted = expr.Replace(RewriteRoot);
304+
return extracted == expr ? null : extracted.Simplify();
305+
306+
bool IsRootOfASum(Entity node)
307+
=> node is Powf(Sumf or Minusf, Number.Rational and not Number.Integer);
308+
309+
Entity RewriteRoot(Entity node)
310+
{
311+
if (!IsRootOfASum(node)
312+
|| node is not Powf(var @base, var power)
313+
|| !TreeAnalyzer.TryGetPolynomial(@base, x, out var monomials))
314+
return node;
315+
var degree = monomials.Keys.Aggregate(EInteger.Zero, EInteger.Max);
316+
if (degree.CompareTo(EInteger.One) < 0)
317+
return node;
318+
var growth = MathS.Pow(x, Number.Integer.Create(degree));
319+
return MathS.Pow(x, Number.Integer.Create(degree) * power) * MathS.Pow((@base / growth).Simplify(), power);
320+
}
321+
}
322+
323+
/// <summary>
324+
/// How many times a single limit may be broken down as a difference of two divergent
325+
/// parts. The conjugate below turns one difference into another, and every part of it
326+
/// is a limit in its own right, so without a bound the work would multiply.
327+
/// </summary>
328+
private const int MaxDifferenceDepth = 2;
329+
330+
[ThreadStatic] private static int differenceDepth;
331+
332+
/// <summary>
333+
/// Which infinity an already computed limit is, or 0 if it is finite or not a number.
334+
/// </summary>
335+
private static int InfiniteSign(Entity? limit)
336+
=> limit?.Evaled is Real { IsFinite: false, IsNaN: false } real ? (real.IsNegative ? -1 : 1) : 0;
337+
338+
/// <summary>
339+
/// oo - oo, which says nothing on its own: whichever of the two grows faster decides
340+
/// the answer, and if neither does the difference can still be finite. Two of the
341+
/// standard readings are covered -- one part outgrowing the other, and the conjugate
342+
/// for a difference containing a root.
343+
/// </summary>
344+
private static Entity? SolveAsDifferenceOfInfinities(Entity expr, Variable x)
345+
{
346+
if (differenceDepth >= MaxDifferenceDepth || expr is not (Sumf or Minusf))
347+
return null;
348+
var terms = Sumf.LinearChildren(expr).ToArray();
349+
if (terms.Length != 2)
350+
return null;
351+
var dest = Real.PositiveInfinity;
352+
differenceDepth++;
353+
try
354+
{
355+
var (firstSign, secondSign) =
356+
(InfiniteSign(ComputeLimit(terms[0], x, dest)), InfiniteSign(ComputeLimit(terms[1], x, dest)));
357+
if (firstSign == 0 || secondSign != -firstSign)
358+
return null;
359+
// Written as minuend - subtrahend with both parts tending to +oo, so that the
360+
// reading below does not have to carry the signs around with it.
361+
var (minuend, subtrahend) = firstSign > 0
362+
? (terms[0], -terms[1])
363+
: (terms[1], -terms[0]);
364+
365+
// The faster growing part decides the answer whenever there is one, which is
366+
// what the ratio of the two says: lim x -> +oo e^x - x is +oo because x / e^x
367+
// tends to 0, and -oo the other way round. This settles nothing when the two
368+
// grow alike, and the ratio then tends to 1 rather than to 0 or to infinity.
369+
if (ComputeLimit((subtrahend / minuend).Simplify(), x, dest) is { } ratio)
370+
{
371+
if (ratio.Evaled == Number.Integer.Zero)
372+
return Real.PositiveInfinity;
373+
if (InfiniteSign(ratio) > 0)
374+
return Real.NegativeInfinity;
375+
}
376+
377+
// a - b = (a^2 - b^2) / (a + b), an identity wherever a + b is not zero, which
378+
// it is not on the way to +oo. It is only an improvement when squaring removes
379+
// a root, and then the leading terms cancel in the numerator and what is left
380+
// is an ordinary quotient: sqrt(x^2 + x) - x becomes x / (sqrt(x^2 + x) + x).
381+
if (!ContainsRadical(minuend, x) && !ContainsRadical(subtrahend, x))
382+
return null;
383+
MultithreadingFunctional.ExitIfCancelled();
384+
var conjugate = ((minuend * minuend - subtrahend * subtrahend) / (minuend + subtrahend)).Simplify();
385+
if (conjugate == expr)
386+
return null;
387+
// Without the roots the numerator is an ordinary polynomial and the denominator
388+
// is what it was, so the quotient either falls to the solvers directly or it is
389+
// no better than what it replaced. Differentiating it repeatedly is a long way
390+
// round to the same nothing, and it is most of the cost here.
391+
suppresslHopital = true;
392+
try { return ComputeLimit(conjugate, x, dest); }
393+
finally { suppresslHopital = false; }
394+
}
395+
finally { differenceDepth--; }
396+
}
397+
398+
private static bool ContainsRadical(Entity expr, Variable x)
399+
=> expr.Nodes.Any(node =>
400+
node is Powf(var @base, Number.Rational and not Number.Integer) && @base.ContainsNode(x));
401+
220402
private static Entity ApplyTrivialTransformations(Entity expr, Variable x, Entity dest, Func<Entity, Entity, Entity> transformation)
221403
=> expr switch
222404
{
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//
2+
// Copyright (c) 2019-2022 Angouri.
3+
// AngouriMath is licensed under MIT.
4+
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
5+
// Website: https://am.angouri.org.
6+
//
7+
8+
using System;
9+
using System.Threading.Tasks;
10+
using AngouriMath;
11+
using AngouriMath.Extensions;
12+
using Xunit;
13+
14+
namespace AngouriMath.Tests.Calculus
15+
{
16+
/// <summary>
17+
/// oo - oo, and the roots of polynomials that so often produce it. Every solver reads
18+
/// either a polynomial or a substitution of +oo, and a root of a sum is neither, so a
19+
/// difference of two of them was left unevaluated however plainly it converged.
20+
/// </summary>
21+
public sealed class DifferenceAtInfinityTest
22+
{
23+
// Compared as numbers, since "1/2" parses as a division of two integers rather than as
24+
// the rational the limit answers with.
25+
private static void AssertLimit(string expression, string destination, string expected) =>
26+
Assert.Equal(
27+
expected.ToEntity().Evaled,
28+
expression.ToEntity().Limit("x", destination.ToEntity()).Evaled);
29+
30+
// One part outgrowing the other decides the answer, which is what the ratio of the
31+
// two says.
32+
[Theory]
33+
[InlineData("e ^ x - x", "+oo", "+oo")]
34+
[InlineData("x - e ^ x", "+oo", "-oo")]
35+
[InlineData("x - ln(x)", "+oo", "+oo")]
36+
[InlineData("ln(x) - x", "+oo", "-oo")]
37+
[InlineData("x ^ 2 - x", "+oo", "+oo")]
38+
[InlineData("x - x ^ 2", "+oo", "-oo")]
39+
[InlineData("sqrt(x) - ln(x)", "+oo", "+oo")]
40+
[InlineData("e ^ x - x ^ 10", "+oo", "+oo")]
41+
public void TheFasterGrowingPartDecides(string expression, string destination, string expected) =>
42+
AssertLimit(expression, destination, expected);
43+
44+
// a - b = (a^2 - b^2) / (a + b) removes the roots from the numerator, and what is
45+
// left of it after the leading terms cancel is an ordinary quotient.
46+
[Theory]
47+
[InlineData("sqrt(x ^ 2 + x) - x", "+oo", "1/2")]
48+
[InlineData("sqrt(x ^ 2 + 1) - x", "+oo", "0")]
49+
[InlineData("x - sqrt(x ^ 2 - x)", "+oo", "1/2")]
50+
[InlineData("sqrt(x + 1) - sqrt(x)", "+oo", "0")]
51+
[InlineData("sqrt(x ^ 2 + 4 * x) - x", "+oo", "2")]
52+
[InlineData("sqrt(x ^ 2 + x) + x", "-oo", "-1/2")]
53+
public void ADifferenceOfRootsGoesThroughItsConjugate(string expression, string destination, string expected) =>
54+
AssertLimit(expression, destination, expected);
55+
56+
// sqrt(x^2 + x) written as x * sqrt(1 + 1/x) says its own growth, and then +oo can
57+
// simply be substituted into what is left. An odd degree is not covered: the growth of
58+
// sqrt(x^3 + x) is x^(3/2), and a quotient with that in it is one l'Hopital's rule is
59+
// stopped from working on, so sqrt(x^3 + x) / x^2 is still left unevaluated.
60+
[Theory]
61+
[InlineData("sqrt(x ^ 2 + x) / x", "+oo", "1")]
62+
[InlineData("x / sqrt(x ^ 2 + 1)", "+oo", "1")]
63+
[InlineData("sqrt(4 * x ^ 2 + 1) / x", "+oo", "2")]
64+
[InlineData("sqrt(x ^ 2 + 1) / sqrt(x ^ 2 + 3 * x)", "+oo", "1")]
65+
[InlineData("sqrt(x ^ 2 - x) / x", "+oo", "1")]
66+
[InlineData("sqrt(x ^ 4 + x) / x ^ 2", "+oo", "1")]
67+
[InlineData("sqrt(x ^ 6 + 1) / x ^ 3", "+oo", "1")]
68+
public void ARootOfAPolynomialSaysHowFastItGrows(string expression, string destination, string expected) =>
69+
AssertLimit(expression, destination, expected);
70+
71+
// The forms that already had an answer must keep it, including the ones where the two
72+
// parts tend to the same infinity and so are not indeterminate at all.
73+
[Theory]
74+
[InlineData("x + sqrt(x)", "+oo", "+oo")]
75+
[InlineData("sqrt(x ^ 2 + x) + x", "+oo", "+oo")]
76+
[InlineData("x - x", "+oo", "0")]
77+
[InlineData("(x ^ 2 + 1) / (x ^ 2 - 1)", "+oo", "1")]
78+
[InlineData("1 / x - 1 / x ^ 2", "+oo", "0")]
79+
[InlineData("(1 + 1/x) ^ x", "+oo", "e")]
80+
public void EstablishedLimitsAreUnaffected(string expression, string destination, string expected) =>
81+
AssertLimit(expression, destination, expected);
82+
83+
/// <summary>
84+
/// Neither reading settles this one: the two parts grow alike, so the ratio says
85+
/// nothing, and the conjugate is a quotient the solvers cannot read either. Leaving it
86+
/// unevaluated is honest; taking a long time over it is not, and the conjugate used to
87+
/// be handed to l'Hopital's rule, which spent seconds arriving at the same nothing.
88+
/// </summary>
89+
[Fact]
90+
public void AFormNeitherReadingSettlesTerminatesWithoutClaimingAnAnswer()
91+
{
92+
var task = Task.Run(() =>
93+
"sqrt(x ^ 2 + 3 * x) - sqrt(x ^ 2 + 1)".ToEntity().Limit("x", "+oo".ToEntity()));
94+
Assert.True(task.Wait(TimeSpan.FromSeconds(30)), "the limit did not terminate");
95+
Assert.IsType<Entity.Limitf>(task.Result);
96+
}
97+
}
98+
}

‎Sources/Tests/UnitTests/Calculus/LimitAtInfinityTest.cs‎

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,25 +66,22 @@ public void EstablishedLimitsAreUnaffected(string expression, string destination
6666
Assert.Equal(expected.ToEntity(), Limit(expression, destination).Simplify());
6767

6868
/// <summary>
69-
/// Differentiating both parts of x / sqrt(x^2 + 1) gives its own reciprocal, so the rule
70-
/// never settles and has to be stopped; x^20 / e^x would settle but only after more
69+
/// Differentiating both parts of (x + sin(x)) / x gives back something no closer to an
70+
/// answer, so the rule has to be stopped; x^20 / e^x would settle but only after more
7171
/// steps than the bound allows. Either way the limit is left unevaluated, which is
7272
/// honest. What must not happen is a hang, and the answer must not become NaN either,
7373
/// since NaN asserts that the limit does not exist.
7474
/// </summary>
7575
/// <remarks>
76-
/// The last two are the forms that showed a bound on the number of steps is not by
77-
/// itself a bound on the work. Each step asks what the two parts of its quotient tend
78-
/// to, and those are limits the rule may be applied to in turn, so sixteen steps deep
79-
/// is far more than sixteen steps: both of these ran for over twenty seconds before the
80-
/// rule was stopped from going round a cycle or handing on a quotient bigger than the
81-
/// one it came from.
76+
/// The last is the form that showed a bound on the number of steps is not by itself a
77+
/// bound on the work. Each step asks what the two parts of its quotient tend to, and
78+
/// those are limits the rule may be applied to in turn, so sixteen steps deep is far
79+
/// more than sixteen steps: it ran for over twenty seconds before the rule was stopped
80+
/// from going round a cycle or handing on a quotient bigger than the one it came from.
8281
/// </remarks>
8382
[Theory]
84-
[InlineData("x / sqrt(x ^ 2 + 1)")]
8583
[InlineData("(x + sin(x)) / x")]
8684
[InlineData("x ^ 20 / e ^ x")]
87-
[InlineData("sqrt(x ^ 2 - x) / x")]
8885
[InlineData("x ^ (3/2) * sqrt(1 + 1 / x ^ 2) / x ^ 2")]
8986
public void AFormTheRuleCannotSettleTerminatesWithoutClaimingAnAnswer(string expression)
9087
{

0 commit comments

Comments
 (0)