From 8e309276612a64d7547d0b50195c95a7eecbcadf Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 4 Nov 2026 09:11:30 +0000 Subject: [PATCH] Read a limit's value at a continuous point before simplifying A limit at a finite point moves the variable out to infinity, x = a +- 1/x, and simplified that whole expression before reading its value there -- which is what the substitution answers a continuous point with in any case, under the same guards for a jump, a cut and an indeterminate power. Simplifying was the expensive half: x / (tanh(1/2) - tan(x)) at 1 took 10 s and tanh(x) / (tanh(1/2) - tan(x)) 18 s, for values read in milliseconds. The substitution is now asked first as well, and answers where its value is a finite real; a complex one can be the value on a cut, whose guards were written against the simplified form. A tangent can come back as the quotient of its sine and cosine. Reading first hands the descent an argument's limit folded -- -1 rather than -1 - 1/+oo -- and the factorial's descent put it straight into x!, writing down the pole: x! at -1 was (-1)!. The factorial declines at a negative integer now. limitcheck, DEPTH=3, seeds 14, 16, 19, 23, 26, 28, 29 against master f3e514ad7: 667 s -> 437 s, "did not finish in 5 s" 13 -> 1, compared 36275 -> 36294, the wrong values unchanged. "NaN where the values settle" 84 -> 85: two wrong NaNs gone (abs(x + 1 - x^x) at 0 from each side, now unevaluated), three limits NaN that were unevaluated, each an expression with no real values on a side, which is the convention x^x at 0 is pinned to. Found at SEED=28; no issue was filed. Co-Authored-By: Claude Opus 5.5 --- BREAKING-CHANGES.md | 18 +++++++ .../Limits/Solvers/Limit.Classes.cs | 17 +++++-- .../Limits/Solvers/Solvers.Definition.cs | 13 ++++- ...tinuousPointIsReadBeforeSimplifyingTest.cs | 48 +++++++++++++++++++ 4 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/AContinuousPointIsReadBeforeSimplifyingTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 9bab3fdbe..a603de820 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -240,6 +240,24 @@ compiled `0^(-1/2)` was 0; since that is `NaN`, the root was missed. | `"sqrt(-4 - 4x) + sqrt(2 + x) = -3 - 4x".ToEntity().Solve("x")` | `{ -1.1951718122545800, -1 }` | `{ -1, … }`, the second root in radicals | | `"sqrt(x + 5) + sqrt(x) = x".ToEntity().Solve("x")` | `{ 5.6349218075063181 }` | the same root in radicals | +### A limit at a point of continuity is read before it is simplified + +**A limit at a finite point takes its value at the point before simplifying the expression, which was the +expensive half: `x / (tanh(1/2) - tan(x))` at 1 took 10 s, and takes 0.1 s.** The value is the same, and its form +can differ: a tangent comes back as the quotient of its sine and cosine. A factorial now declines at its poles, which +reading the value first would otherwise have written down. Some answers move, in both directions, for an expression +whose values are not real on a side. A NaN there follows the convention `x^x` at 0 is pinned to, a side with no real +values making no two-sided limit, where the limit was unevaluated. + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"x / (tanh(1/2) - tan(x))".ToEntity().Limit("x", 1)` | `1 / ((e - 1) / (e + 1) - tan(1))`, in 4.5 s | `1 / ((e - 1) / (e + 1) - sin(1) / cos(1))` | +| `"abs(x + 1 - x^x)".ToEntity().Limit("x", 0, ApproachFrom.Right)` | `NaN` | `limitright(abs(x + 1 - x ^ x), x, 0)` | +| `"abs(x + 1 - x^x)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `NaN` | `limitleft(abs(x + 1 - x ^ x), x, 0)` | +| `"sin(x)^(log(x, pi)^arcsin(x))".ToEntity().Limit("x", 1)` | `limit(sin(x) ^ log(x, pi) ^ arcsin(x), x, 1)` | `NaN` | +| `"(2*(x - pi))^ln(-x)".ToEntity().Limit("x", 0)` | `limit((2 * (x - pi)) ^ ln(-x), x, 0)` | `NaN` | +| `"arctan((-1)^x)^(pi/x/cos(2))".ToEntity().Limit("x", 1/2)` | `limit(arctan((-1) ^ x) ^ (pi / x / cos(2)), x, 1/2)` | `NaN` | + ### A radical times a power of its variable is solved exactly **Silent: an equation in `sqrt(x)` or a cube root of `x` beside powers of `x` is answered in radicals and with all its diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs index 50931b529..3f5fe7713 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs @@ -419,11 +419,18 @@ partial record Arccotanf partial record Factorialf { - internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) => - ComputeLimitImpl(this, x, dist, side) is { } lim ? lim - : ComputeLimitImpl(New( - Argument.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim1 ? lim1 : Argument), - x, dist, side); + // Not at a pole: the factorial of a negative integer has no value, and the factorial tends + // to an infinity there whose sign depends on the side, so putting the argument's limit in + // would only write the pole down. x! at -1 came back (-1)! once that limit arrived as -1. + internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) + { + if (ComputeLimitImpl(this, x, dist, side) is { } lim) + return lim; + var argumentLimit = Argument.ComputeLimitDivideEtImpera(x, dist, side); + if (argumentLimit?.Evaled is Integer { EInteger.Sign: < 0 }) + return null; + return ComputeLimitImpl(New(argumentLimit is { IsFinite: true } lim1 ? lim1 : Argument), x, dist, side); + } } // The upper incomplete gamma function, continuous in z away from 0, and at 0 for Re a > 0, where it is Gamma(a): the diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs index 33cc7480e..3e2ec8778 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs @@ -57,7 +57,18 @@ private static Entity AsAPowerOfTheVariable(Entity node, Variable x) private static Entity? SimplifyAndComputeLimitToInfinityCore(Entity expr, Variable x) { - expr = expr.Replace(node => AsAPowerOfTheVariable(node, x)).Simplify(); + expr = expr.Replace(node => AsAPowerOfTheVariable(node, x)); + // The value at the destination, read before the simplification below as well as after + // it: the substitution is what answers a continuous point in any case, under the same + // guards for a jump, a cut and an indeterminate power, and simplifying first is the + // expensive half. x / (tanh(1/2) - tan(x)) at 1, moved out to infinity as + // x = 1 + 1/x, took 10 s to simplify for a value the substitution reads in + // milliseconds. A real number only: a complex one can be the value on a cut, and the + // guards for those after this were written against the simplified form. + if (LimitSolvers.SolveBySubstitution(expr, x) is { } beforeSimplifying + && beforeSimplifying.Evaled is Real { IsFinite: true }) + return beforeSimplifying.InnerSimplified; + expr = expr.Simplify(); if (expr is Providedf(var expression, _)) expr = expression; // limits operate assuming a continuous expression even though some points may be undefined. // The second remarkable limit reads a 1^oo, and it ran once, at the top of diff --git a/Sources/Tests/UnitTests/Calculus/AContinuousPointIsReadBeforeSimplifyingTest.cs b/Sources/Tests/UnitTests/Calculus/AContinuousPointIsReadBeforeSimplifyingTest.cs new file mode 100644 index 000000000..99f486480 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AContinuousPointIsReadBeforeSimplifyingTest.cs @@ -0,0 +1,48 @@ +// +// 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; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A limit at a finite point is taken by moving the variable out to infinity, and the expression was simplified there + /// before its value was read: x / (tanh(1/2) - tan(x)) at 1, a point where it is continuous, took 10 s, and + /// tanh(x) / (tanh(1/2) - tan(x)) 18 s, for a value the substitution reads in milliseconds. Found by + /// limitcheck at DEPTH=3, SEED=28, as twelve limits that did not finish in 5 s; no issue was filed. + /// + [Trait("Area", "Calculus")] + public sealed class AContinuousPointIsReadBeforeSimplifyingTest + { + // The cap is far from both: each takes about 0.1 s, and took 10 to 18 s. + [Theory] + [InlineData("x / (tanh(1/2) - tan(x))", "1", ApproachFrom.BothSides)] + [InlineData("tanh(x) / (tanh(1/2) - tan(x))", "1", ApproachFrom.BothSides)] + [InlineData("tanh(x) / (tanh(1/2) - tan(x))", "-1", ApproachFrom.Left)] + [InlineData("tanh(x) / (tanh(1/2) - tan(x))", "2", ApproachFrom.Right)] + public void IsTheValueThereAndPrompt(string input, string destination, ApproachFrom side) + { + var expression = MathS.FromString(input); + var task = Task.Factory.StartNew(() => expression.Limit("x", destination, side), TaskCreationOptions.LongRunning); + Assert.True(task.Wait(TimeSpan.FromSeconds(5)), $"lim {side} x->{destination} {input} did not finish in five seconds"); + var difference = (task.Result - expression.Substitute("x", destination)).EvalNumerical(); + Assert.True(difference is Entity.Number.Complex value && Entity.Number.Complex.Abs(value) is Entity.Number.Real abs && abs < 1e-10, + $"lim {side} x->{destination} {input} = {task.Result}"); + } + + // A pole of the factorial is not a value to read: x! at -1 has an infinity of each sign on its two sides. + [Fact] + public void TheFactorialAtAPoleIsNotItsValue() + { + Assert.IsType(MathS.FromString("x!").Limit("x", -1, ApproachFrom.Left)); + Assert.IsType(MathS.FromString("x!").Limit("x", -1, ApproachFrom.Right)); + } + } +}