From f691f25207f09d6b38283a752b6180b03ea82659 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 5 Aug 2026 09:53:28 +0000 Subject: [PATCH] Settle a difference of squared reciprocals instead of denying it (#727) lim x->0 (1/x^2 - 1/sin(x)^2) answered NaN, which is not "unevaluated" but the claim that the limit does not exist. It is -1/3. The mirrored difference was the same and csc(x)^2 - 1/x^2 came back unevaluated where it is 1/3. The difference is put over a common denominator already, and what comes out is an ordinary 0/0 -- (sin(x)^2 - x^2) / (x^2 * sin(x)^2). l'Hopital's rule refused its first step. The rule's growth guard allowed eight nodes, which is what growth looks like when the divisor is a power of the variable: differentiating that shrinks it, so only the dividend grows and it grows by addition. A divisor that is a product of vanishing factors grows by the product rule instead, so the chain grows before it collapses -- 17 -> 26 -> 33 -> 46 nodes -- and four steps then settle it. The guard is now the larger of eight nodes and three fifths again, proportional where the growth is proportional and unchanged for the small quotients it was measured on. It is wider than the old rule everywhere, so nothing that answered before is turned away now, and it costs nothing on the shape the flat budget was measured against: x^(3/2) * sqrt(1 + 1/x^2) / x^2 at +oo grows 19 -> 31 nodes at its first step, which is over both budgets, so it stops where it stopped before and still answers 0 in 240 ms. That step adds twelve nodes and the next twenty-six; the old comment's eighteen was measured on an earlier build and is corrected here. The cosecant is a second cause, not the same one. csc(x) is written 1/sin(x) in front of the descent, so csc(x)^2 arrives as (1/sin(x))^2 with its denominator inside the power, and SplitProduct did not look there -- the term was read as having no denominator and the sum was never combined at all. Only whole powers are split this way: (a/b)^n is a^n/b^n exactly for those, while at a half it is not, and the limit of the rewritten form would be the limit of a different function. lim x->0 (1/x^2 - 1/sin(x)^2) NaN -> -1/3 lim x->0 (1/sin(x)^2 - 1/x^2) NaN -> 1/3 lim x->0 (csc(x)^2 - 1/x^2) unevaluated -> 1/3 4855 unit tests and 130 F# tests pass; corpus unchanged at 112/117 with no wrong answers, errors or timeouts, and its total time within noise. Co-Authored-By: Claude Opus 5 --- .../Continuous/Limits/Transformations.cs | 52 ++++++++++++++++--- .../Calculus/DifferenceOfFractionsTest.cs | 18 +++++++ 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs index 03f8400c1..07905206e 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs @@ -420,16 +420,37 @@ private static bool AlreadyBeingDifferentiated(Entity quotient) /// /// Whether differentiating both parts has left a quotient bigger than the one it came - /// from by more than the derivative of a power or a logarithm accounts for. Each step - /// asks what the two parts of its quotient tend to, and a bigger quotient makes those - /// two questions harder than the one being answered, so a step that grows is a step - /// away from an answer rather than towards one. The room left over is measured: the - /// steps that do reach an answer add two nodes, and the most any of them was seen to - /// add is six, while one step of x^(3/2) * sqrt(1 + 1/x^2) / x^2 adds eighteen and - /// leaves the rule tens of seconds of work that ends in nothing. + /// from by more than a step on the way to an answer accounts for. Each step asks what + /// the two parts of its quotient tend to, and a bigger quotient makes those two + /// questions harder than the one being answered, so a step that grows without bound is + /// a step away from an answer rather than towards one. One step of + /// x^(3/2) * sqrt(1 + 1/x^2) / x^2 adds twelve nodes and the next adds twenty-six, + /// which is that shape. /// + /// + /// The room was a flat eight nodes, which is what growth looks like when the divisor is + /// a power of the variable: differentiating that shrinks it, so the only thing that + /// grows is the dividend and it grows by addition. A divisor that is a *product* of + /// vanishing factors grows by the product rule instead, which is multiplication -- + /// x^2 * sin(x)^2 differentiates into a sum of two products, each about the size of the + /// whole. Such a chain still terminates, since the order at which the divisor vanishes + /// falls by one at every step, but it grows for as long as it takes to get there and + /// only then collapses: 1/x^2 - 1/sin(x)^2 at 0 goes 17 -> 26 -> 33 -> 46 nodes before + /// four steps settle it at -1/3. A flat budget cannot say that, and turned the first of + /// those steps away, leaving the descent to answer +oo - +oo -- NaN, the claim that the + /// limit does not exist -- for a limit that exists. + /// + /// So the room is the larger of the eight nodes and three fifths again, which is + /// proportional where the growth is proportional and unchanged for the small quotients + /// the flat budget was measured on. It is wider than the old rule everywhere, so no + /// chain that reached an answer before is turned away now. It costs nothing on the + /// shape that motivated the flat budget either: nineteen nodes to thirty-one is above + /// three fifths again as surely as it is above eight more, so that chain still stops at + /// the same step, and the limit still answers 0 in the same 240 ms. The corpus is + /// unchanged at 112/117 and, in total time, within noise of where it was. + /// private static bool GrewTooMuch(Entity quotient, Entity applied) - => applied.Nodes.Count() > quotient.Nodes.Count() + 8; + => applied.Nodes.Count() > Math.Max(quotient.Nodes.Count() + 8, quotient.Nodes.Count() * 8 / 5); [ThreadStatic] private static bool suppresslHopital; @@ -438,12 +459,27 @@ private static bool GrewTooMuch(Entity quotient, Entity applied) /// taken into the latter. A product with no reciprocal factor in it comes back with a /// denominator of 1, which is what makes this usable term by term below. /// + /// + /// A power of a quotient is one of these too, and is how a squared cosecant arrives: + /// csc(x) is written 1/sin(x) in front of the descent, so csc(x)^2 reaches here as + /// (1/sin(x))^2 with its denominator inside the power rather than at the top. Read as + /// a factor with no denominator, csc(x)^2 - 1/x^2 at 0 was never put over a common + /// denominator and came back unevaluated where it is 1/3. + /// + /// Only for a whole power, where (a/b)^n is a^n/b^n exactly. At a half it is not: the + /// square root of 1/(-1) is i and the quotient of the two square roots is -i, and a + /// limit taken of the second is a limit of a different function. + /// private static (Entity Numerator, Entity Denominator) SplitProduct(Entity expr) { Entity numerator = 1, denominator = 1; foreach (var factor in Mulf.LinearChildren(expr)) switch (factor) { + case Powf(Divf(var dividend, var divisor), Integer { IsPositive: true } power): + numerator *= dividend.Pow(power); + denominator *= divisor.Pow(power); + break; case Powf(var @base, Real { IsNegative: true } power): denominator *= @base.Pow(-power); break; diff --git a/Sources/Tests/UnitTests/Calculus/DifferenceOfFractionsTest.cs b/Sources/Tests/UnitTests/Calculus/DifferenceOfFractionsTest.cs index 05cee66c1..a26b447c8 100644 --- a/Sources/Tests/UnitTests/Calculus/DifferenceOfFractionsTest.cs +++ b/Sources/Tests/UnitTests/Calculus/DifferenceOfFractionsTest.cs @@ -37,6 +37,24 @@ private static void AssertLimit(string expression, string destination, string ex public void ADifferenceOfDivergentFractionsGoesOverACommonDenominator(string expression, string destination, string expected) => AssertLimit(expression, destination, expected); + /// + /// https://github.com/asc-community/AngouriMath/issues/727. + /// + /// The same difference with both denominators squared. Putting it over a common + /// denominator is not enough on its own here: the denominator that comes out is a + /// product of two vanishing factors, so l'Hopital's rule differentiates it by the + /// product rule and the quotient grows at every step before it collapses. The rule + /// used to refuse the first of those steps and leave the descent to answer + /// +oo - +oo, which is NaN -- the claim that the limit does not exist -- where it + /// is -1/3. + /// + [Theory] + [InlineData("1/x ^ 2 - 1/sin(x) ^ 2", "0", "-1/3")] + [InlineData("1/sin(x) ^ 2 - 1/x ^ 2", "0", "1/3")] + [InlineData("csc(x) ^ 2 - 1/x ^ 2", "0", "1/3")] + public void ADifferenceOfSquaredFractionsIsSettledToo(string expression, string destination, string expected) => + AssertLimit(expression, destination, expected); + /// /// A sum is only put over a common denominator where a denominator contains the variable /// -- those are the ones that vanish or diverge and make the difference indeterminate.