Skip to content

Commit 1c5fb82

Browse files
Put a difference of two divergent fractions over one denominator (#699)
* Put a difference of two divergent fractions over one denominator Nothing in the descent takes a difference apart any further than asking what each half tends to, so 1/x - 1/sin(x) at 0 came out as +oo - +oo on the right and -oo - -oo on the left, and NaN either way -- the claim that the limit does not exist, where it is 0. Written over the common denominator the same expression is (sin(x) - x) / (x * sin(x)), an ordinary 0/0 that l'Hopital's rule settles in three steps. The rewrite goes where the products already went. AsQuotient existed to hand the rule the quotients that are not written as one, and a sum of fractions is the second kind of those; it is now the one place that answers what the numerator and the denominator of an expression are, for a product and for a sum of them alike. Only where a denominator contains the variable, since it is the denominators that vanish or diverge that make the difference indeterminate, and combining over a constant gains nothing while still costing the rule an expression to differentiate. Only up to three terms: each one multiplies the numerator by every other term's denominator, so the expression grows with the square of the count, and two or three is what the forms that need this are written with. Answers 1/x - 1/sin(x) and 1/sin(x) - 1/x at 0, csc(x) - cotan(x) at 0, and 1/ln(x) - 1/(x - 1) at 1 either way round. Five of the eleven new tests fail without it. Corpus unchanged at 101/117 with 0 wrong; suite 4020 passed, 0 failed. * Write a tangent as a quotient before combining the fractions tan(x) is opaque to everything that reads quotients: it is neither a quotient nor a negative power, so the split puts it into the numerator whole and 1/x - 1/tan(x) comes out over x * tan(x). That is the right answer written in the wrong form. The rules do reach 0 through it, but only after rewriting the whole expression dozens of ways, and lim x->0+ (1/sin(x) - 1/tan(x)) took forty seconds against under one for the same limit over sin(x) * sin(x). Not with the other trigonometric rewrite, which runs in front of the first remarkable limit. That one matches a quotient, and rewriting tan(b*x - x) as a quotient of its own turns the quotient it sits under into a product: lim x->0 sin(x - a*x) / tan(b*x - x), which the suite pins at (a-1)/(1-b), stops being read at all. Measured, not reasoned about -- it is what the test caught when the rewrite was put there first. Suite 4020 passed, 0 failed, unchanged.
1 parent a2cc479 commit 1c5fb82

3 files changed

Lines changed: 144 additions & 14 deletions

File tree

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ private static Entity ExpandLogarithm(Entity expr)
6060
expr = ApplyTrivialTransformations(expr, x, dest, (_, exprLim) => exprLim);
6161
expr = ApplyFirstRemarkable(expr, x, dest);
6262
expr = expr.Replace(c => ApplySecondRemarkable(c, x, dest));
63+
// A tangent is a quotient as much as a cosecant is a reciprocal, and leaving it
64+
// written as one function makes it opaque to everything that reads quotients --
65+
// including the split that puts a difference of fractions over one denominator,
66+
// which takes tan(x) into the numerator whole and so combines 1/x - 1/tan(x) over
67+
// x * tan(x). That is the right answer in the wrong form: the rules do reach 0
68+
// through it, but only after rewriting the whole expression dozens of ways, and
69+
// lim x->0+ (1/sin(x) - 1/tan(x)) took forty seconds against under one for the
70+
// same limit over sin(x) * sin(x).
71+
//
72+
// After the first remarkable limit and not with the trigonometric rewrite in front
73+
// of it. That one matches a quotient, and rewriting tan(b*x - x) as a quotient of
74+
// its own turns the quotient it sits under into a product:
75+
// lim x->0 sin(x - a*x) / tan(b*x - x) stops being read at all.
76+
expr = expr.Replace(c => AsSineOverCosine(c, x));
6377
MultithreadingFunctional.ExitIfCancelled();
6478

6579
if (side is ApproachFrom.Left or ApproachFrom.Right)

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

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -196,39 +196,85 @@ private static bool GrewTooMuch(Entity quotient, Entity applied)
196196
[ThreadStatic] private static bool suppresslHopital;
197197

198198
/// <summary>
199-
/// A product that has a reciprocal factor in it, rewritten as a quotient, or
200-
/// <see langword="null"/> if it has none. The rule below only reads quotients, so
201-
/// <c>lim x -&gt; +oo x^4 * e^(-x)</c> came out as NaN while the same limit written
202-
/// <c>x^4 / e^x</c> gave 0 --
203-
/// https://github.com/asc-community/AngouriMath/issues/596.
199+
/// One product broken into a numerator and a denominator, with every reciprocal factor
200+
/// taken into the latter. A product with no reciprocal factor in it comes back with a
201+
/// denominator of 1, which is what makes this usable term by term below.
204202
/// </summary>
205-
private static Entity? AsQuotient(Entity expr)
203+
private static (Entity Numerator, Entity Denominator) SplitProduct(Entity expr)
206204
{
207-
if (expr is not Mulf)
208-
return null;
209205
Entity numerator = 1, denominator = 1;
210-
var reciprocal = false;
211206
foreach (var factor in Mulf.LinearChildren(expr))
212207
switch (factor)
213208
{
214209
case Powf(var @base, Real { IsNegative: true } power):
215210
denominator *= @base.Pow(-power);
216-
reciprocal = true;
217211
break;
218212
case Powf(var @base, Mulf(Real { IsNegative: true } coefficient, var rest)):
219213
denominator *= @base.Pow(-coefficient * rest);
220-
reciprocal = true;
221214
break;
222215
case Divf(var dividend, var divisor):
223216
numerator *= dividend;
224217
denominator *= divisor;
225-
reciprocal = true;
226218
break;
227219
default:
228220
numerator *= factor;
229221
break;
230222
}
231-
return reciprocal ? numerator / denominator : null;
223+
return (numerator, denominator);
224+
}
225+
226+
/// <summary>
227+
/// How many terms of a sum are worth putting over a common denominator. Each one
228+
/// multiplies the numerator by every other term's denominator, so the expression grows
229+
/// with the square of the count, and the rule below has to differentiate whatever comes
230+
/// out of it. Two and three terms is what the forms that need this are written with.
231+
/// </summary>
232+
private const int MaxTermsOverACommonDenominator = 3;
233+
234+
/// <summary>
235+
/// The expression rewritten as a single quotient, or <see langword="null"/> where it is
236+
/// not one to begin with and nothing is gained by writing it as one. The rule below only
237+
/// reads quotients, and two kinds of expression are quotients without being written as
238+
/// one.
239+
/// <para/>
240+
/// A product with a reciprocal factor: <c>lim x -&gt; +oo x^4 * e^(-x)</c> came out as
241+
/// NaN while the same limit written <c>x^4 / e^x</c> gave 0 --
242+
/// https://github.com/asc-community/AngouriMath/issues/596.
243+
/// <para/>
244+
/// And a sum of them, which is where the differences of two divergent parts at a finite
245+
/// point live: <c>1/x - 1/sin(x)</c> at 0 is +oo - +oo on the right and -oo - -oo on the
246+
/// left, and nothing in the descent takes a difference apart any further than asking
247+
/// what each half tends to. Over the common denominator it is
248+
/// <c>(sin(x) - x) / (x * sin(x))</c>, which is 0/0 and which the rule settles at 0 in
249+
/// three steps.
250+
/// </summary>
251+
private static Entity? AsQuotient(Entity expr, Variable x)
252+
{
253+
if (expr is Mulf)
254+
{
255+
var (numerator, denominator) = SplitProduct(expr);
256+
return denominator == 1 ? null : numerator / denominator;
257+
}
258+
if (expr is not (Sumf or Minusf))
259+
return null;
260+
var terms = Sumf.LinearChildren(expr).ToList();
261+
if (terms.Count > MaxTermsOverACommonDenominator)
262+
return null;
263+
Entity? combined = null, common = null;
264+
var worthIt = false;
265+
foreach (var term in terms)
266+
{
267+
var (numerator, denominator) = SplitProduct(term);
268+
// A denominator that does not contain x is a constant factor and putting the sum
269+
// over it gains nothing, while still costing the rule an expression to
270+
// differentiate. It is the denominators that vanish or diverge that make the
271+
// difference indeterminate, and those are the ones that contain x.
272+
worthIt |= denominator != 1 && denominator.ContainsNode(x);
273+
(combined, common) = combined is null || common is null
274+
? (numerator, denominator)
275+
: (combined * denominator + numerator * common, common * denominator);
276+
}
277+
return worthIt && combined is { } && common is { } ? (combined / common).InnerSimplified : null;
232278
}
233279

234280
/// <remarks>
@@ -258,7 +304,7 @@ private static bool GrewTooMuch(Entity quotient, Entity applied)
258304

259305
private static Entity? ApplylHopitalRuleImpl(Entity expr, Variable x, Entity dest, ApproachFrom side)
260306
{
261-
if (expr is not Divf && AsQuotient(expr) is { } quotient)
307+
if (expr is not Divf && AsQuotient(expr, x) is { } quotient)
262308
expr = quotient;
263309
if (expr is Divf(var num, var den))
264310
if (EvalAssumingContinuous(num.Limit(x, dest, side)) is var numLimit && EvalAssumingContinuous(den.Limit(x, dest, side)) is var denLimit)
@@ -490,6 +536,20 @@ when ComputeLimit(a, x, dest) is { } aLim && ComputeLimit(b, x, dest) is { } bLi
490536
_ => expr
491537
};
492538

539+
/// <summary>
540+
/// A tangent or a cotangent written as the quotient it is. Kept apart from
541+
/// <see cref="TrivialTrigonometricReplacement"/>, which runs in front of the first
542+
/// remarkable limit, because that limit matches a quotient and this rewrite would turn
543+
/// the quotient a tangent sits under into a product.
544+
/// </summary>
545+
private static Entity AsSineOverCosine(Entity expr, Variable x)
546+
=> expr switch
547+
{
548+
Tanf(var arg) when arg.ContainsNode(x) => MathS.Sin(arg) / MathS.Cos(arg),
549+
Cotanf(var arg) when arg.ContainsNode(x) => MathS.Cos(arg) / MathS.Sin(arg),
550+
_ => expr
551+
};
552+
493553
private static Entity TrivialTrigonometricReplacement(Entity expr, Variable x)
494554
=> expr switch
495555
{
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 AngouriMath;
9+
using AngouriMath.Extensions;
10+
using Xunit;
11+
12+
namespace AngouriMath.Tests.Calculus
13+
{
14+
/// <summary>
15+
/// A difference of two fractions that both diverge at the point being approached. Nothing
16+
/// in the descent takes a difference apart any further than asking what each half tends to,
17+
/// and each half here is an infinity, so 1/x - 1/sin(x) at 0 came out as NaN -- the claim
18+
/// that the limit does not exist -- where it is 0. Over the common denominator the same
19+
/// expression is (sin(x) - x) / (x * sin(x)), which is an ordinary 0/0 that l'Hopital's
20+
/// rule settles.
21+
/// </summary>
22+
public sealed class DifferenceOfFractionsTest
23+
{
24+
// Compared as numbers, since "1/2" parses as a division of two integers rather than as
25+
// the rational the limit answers with.
26+
private static void AssertLimit(string expression, string destination, string expected) =>
27+
Assert.Equal(
28+
expected.ToEntity().Evaled,
29+
expression.ToEntity().Limit("x", destination.ToEntity()).Evaled);
30+
31+
[Theory]
32+
[InlineData("1/x - 1/sin(x)", "0", "0")]
33+
[InlineData("1/sin(x) - 1/x", "0", "0")]
34+
[InlineData("csc(x) - cotan(x)", "0", "0")]
35+
[InlineData("1/ln(x) - 1/(x - 1)", "1", "1/2")]
36+
[InlineData("1/(x - 1) - 1/ln(x)", "1", "-1/2")]
37+
public void ADifferenceOfDivergentFractionsGoesOverACommonDenominator(string expression, string destination, string expected) =>
38+
AssertLimit(expression, destination, expected);
39+
40+
/// <summary>
41+
/// A sum is only put over a common denominator where a denominator contains the variable
42+
/// -- those are the ones that vanish or diverge and make the difference indeterminate.
43+
/// Combining over a constant gains nothing and still costs the rule an expression to
44+
/// differentiate.
45+
/// </summary>
46+
[Theory]
47+
[InlineData("x / 2 + x / 3", "+oo", "+oo")]
48+
[InlineData("1/x + 1/x ^ 2", "+oo", "0")]
49+
[InlineData("x - 1/x", "+oo", "+oo")]
50+
[InlineData("(x + 1) / x", "+oo", "1")]
51+
[InlineData("1/x - 1/x", "0", "0")]
52+
[InlineData("x + 1/2", "0", "1/2")]
53+
public void EstablishedLimitsAreUnaffected(string expression, string destination, string expected) =>
54+
AssertLimit(expression, destination, expected);
55+
}
56+
}

0 commit comments

Comments
 (0)