From 3f17a16718c67a7621d584e3082e8d3f32e86c3f Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 3 Nov 2026 23:48:45 +0000 Subject: [PATCH 1/2] Turn l'Hopital away from a quotient whose whole powers grow GrewTooMuch turned the rule away where a quotient grew in nodes, and a chain whose divisor is a reciprocal power, x^n / (1 / g^k), grows in its exponents instead: the derivative of 1/g^k is over g^(2k), so k doubled at each step with the node count flat, and the inner limits expanded the powers. The limit of (-ln(2)/ln(x))^(e^x - 1) at 0 from the right went 6, 12, 24, ... and did not finish in ten minutes; it is 1, which 2.6.0 answered and #2306 stopped answering (bisected: #2306's factor taken out of a sum changed the route into this chain). The spelling with log(x, 1/2) and sgn(x) was 0 in 2.6.0 and hung on master. The sum of the whole exponents is measured as the nodes are, with the same room, the larger of eight more and three fifths again: a chain that is getting somewhere lowers its degrees. Both limits are 1, in 6 s and 3.4 s. limitcheck at SEED=16, and at DEPTH=3 on SEED=19 and 23, changes only the limit that did not finish; 752 limit tests pass. Co-Authored-By: Claude Opus 5.5 --- BREAKING-CHANGES.md | 14 ++++++ .../Continuous/Limits/Transformations.cs | 18 +++++++- ...tWhosePowersGrowIsNotDifferentiatedTest.cs | 44 +++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 Sources/Tests/UnitTests/Calculus/AQuotientWhosePowersGrowIsNotDifferentiatedTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index a82d37cbd..bbc9d7b7f 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -351,6 +351,20 @@ positive on both sides. `x^x` is still `NaN` at 0. | `"(x^2)^x".ToEntity().Limit("x", 0)` | `NaN` | `1` | | `"(x*x)^(x - 1)".ToEntity().Limit("x", 0)` | `NaN` | `+oo` | +### A 0^0 limit whose l'Hopital chain doubles its powers has its value + +**Silent: a different answer.** l'Hopital's rule was turned away where a +quotient grew in nodes, and a chain whose divisor is a reciprocal power, `x^n / (1 / g^k)`, grows in its exponents +instead: the derivative of `1/g^k` is over `g^(2k)`, so k doubled at each step and the chain never ended. The whole +exponents are measured as the nodes are now, with the same room, and the limits below have their value. No issue was +filed; a check of limits against values near the destination found it. + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `limitright((log(x, 1/2))^(exp(x) - sgn(x)), x, 0)` | `0` | `1` | + +The same spelled `(-ln(2)/ln(x))^(exp(x) - 1)` was `1` in 2.6.0 and did not return after it; it is `1` again. + ### A limit of a logarithm that is not decided is left unevaluated **Not silent: what overflowed the stack now returns.** A limit of a logarithm whose argument has no limit, as diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs index e2d124163..99d206605 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs @@ -1264,7 +1264,23 @@ private static bool AlreadyBeingDifferentiated(Entity quotient) /// 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() > Math.Max(quotient.Nodes.Count() + 8, quotient.Nodes.Count() * 8 / 5); + => applied.Nodes.Count() > Math.Max(quotient.Nodes.Count() + 8, quotient.Nodes.Count() * 8 / 5) + || WholePowers(applied) > Math.Max(WholePowers(quotient) + 8, WholePowers(quotient) * 8 / 5); + + /// + /// The whole exponents of 's powers, added up: the growth a node count does not see. + /// + /// + /// (e^(-1/x) - 1)^12 and (e^(-1/x) - 1)^24 have the same nodes, and a chain whose divisor is + /// a reciprocal power, x^n / (1 / g^k), doubles k at every step, since the derivative of + /// 1/g^k is over g^(2k): the limit of (-ln(2)/ln(x))^(e^x - 1) at 0 from the right + /// went 6, 12, 24, ... and did not finish in ten minutes, the inner limits expanding the powers. A + /// derivative lowers the degrees of a chain that is getting somewhere; so the same room as for the + /// nodes, the larger of eight more and three fifths again. + /// + private static int WholePowers(Entity expr) + => expr.Nodes.Sum(static node => node is Powf(_, Integer { EInteger: var exponent }) && exponent.CanFitInInt32() + ? Math.Abs(exponent.ToInt32Unchecked()) : 0); [ThreadStatic] private static bool suppresslHopital; diff --git a/Sources/Tests/UnitTests/Calculus/AQuotientWhosePowersGrowIsNotDifferentiatedTest.cs b/Sources/Tests/UnitTests/Calculus/AQuotientWhosePowersGrowIsNotDifferentiatedTest.cs new file mode 100644 index 000000000..dfd748bd5 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AQuotientWhosePowersGrowIsNotDifferentiatedTest.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using System; +using System.Threading.Tasks; +using AngouriMath.Core; +using Xunit; +using static AngouriMath.Entity; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// l'Hopital's rule was turned away where a quotient grew in nodes, and a chain whose divisor is a reciprocal power, + /// x^n / (1 / g^k), grows in its exponents instead: the derivative of 1/g^k is over g^(2k), so k + /// went 6, 12, 24, ..., and the limit of (-ln(2)/ln(x))^(e^x - 1) at 0 from the right did not finish in ten + /// minutes. It is 1, as 2.6.0 answered; the spelling with log(x, 1/2) and sgn(x), 2.6.0 answered 0. Found by + /// limitcheck at DEPTH=3, SEED=23; no issue was filed. + /// + [Trait("Area", "Calculus")] + public sealed class AQuotientWhosePowersGrowIsNotDifferentiatedTest + { + [Theory] + [InlineData("(-ln(2)/ln(x))^(exp(x) - 1)")] + [InlineData("(log(x, 1/2))^(exp(x) - sgn(x))")] + public void IsOneAtZeroFromTheRight(string input) + { + var task = Task.Factory.StartNew(() => MathS.FromString(input).Limit("x", 0, ApproachFrom.Right), + TaskCreationOptions.LongRunning); + Assert.True(task.Wait(TimeSpan.FromSeconds(60)), $"the limit of {input} at 0 from the right did not finish in a minute"); + Assert.Equal(Number.Integer.One, task.Result); + } + + [Theory] + [InlineData("1/x^2 - 1/sin(x)^2", "-1/3")] + [InlineData("(1 - cos(x)) / x^2", "1/2")] + [InlineData("x^3 / (x - sin(x))", "6")] + public void AChainThatGetsSomewhereIsAsBefore(string input, string expected) + => Assert.Equal(MathS.FromString(expected), MathS.FromString(input).Limit("x", 0)); + } +} From b0bb3ae0d97f0432de0c58aca713a556fc466983 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 4 Nov 2026 00:17:33 +0000 Subject: [PATCH 2/2] Give the l'Hopital chain test five minutes, far from the runner and the hang The 0^0 limit takes about 6 s here and past a minute on CI's Windows runner, whose suite took 19 m 54 s against 7 m; a minute's cap measured the runner. Before the fix it did not finish in ten minutes. Co-Authored-By: Claude Opus 5.5 --- .../AQuotientWhosePowersGrowIsNotDifferentiatedTest.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Tests/UnitTests/Calculus/AQuotientWhosePowersGrowIsNotDifferentiatedTest.cs b/Sources/Tests/UnitTests/Calculus/AQuotientWhosePowersGrowIsNotDifferentiatedTest.cs index dfd748bd5..d3a16cfff 100644 --- a/Sources/Tests/UnitTests/Calculus/AQuotientWhosePowersGrowIsNotDifferentiatedTest.cs +++ b/Sources/Tests/UnitTests/Calculus/AQuotientWhosePowersGrowIsNotDifferentiatedTest.cs @@ -30,7 +30,9 @@ public void IsOneAtZeroFromTheRight(string input) { var task = Task.Factory.StartNew(() => MathS.FromString(input).Limit("x", 0, ApproachFrom.Right), TaskCreationOptions.LongRunning); - Assert.True(task.Wait(TimeSpan.FromSeconds(60)), $"the limit of {input} at 0 from the right did not finish in a minute"); + // Far from both: the limit takes about 6 s here and past a minute on CI's Windows runner, whose suite runs + // three times as long, and before the fix it did not finish in ten minutes. + Assert.True(task.Wait(TimeSpan.FromMinutes(5)), $"the limit of {input} at 0 from the right did not finish in five minutes"); Assert.Equal(Number.Integer.One, task.Result); }