diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs index 6ab6be282..54ca19c0f 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs @@ -295,9 +295,29 @@ partial record Limitf partial record Signumf { - // TODO: internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) - => new Limitf(this, x, dist, side); + { + if (Argument.ComputeLimitDivideEtImpera(x, dist, side) is not { } argument) + return null; + // The sign is constant on either side of zero, so wherever the argument tends + // to anything but zero the limit is simply the sign of that -- including the + // infinities, where it is 1 and -1. At zero it is the one place there is + // nothing to say: the sign is 1 on one side and -1 on the other, and which one + // a one-sided limit takes depends on the direction the argument approaches + // from rather than only on what it tends to. + // + // Nothing is returned there rather than an unevaluated limit of this very + // expression. That is what used to be here, and it does not merely fail to + // answer -- the two-sided path compares its two one-sided results by evaluating + // them, evaluating a limit computes it, and computing it arrives back here. The + // recursion ends by overflowing the stack, which kills the process rather than + // raising anything a caller could catch: + // https://github.com/asc-community/AngouriMath/issues/704. Null says the same + // thing to the caller, which hands back an unevaluated limit of its own. + if (argument.Evaled is not Number value || value == 0 || value.Evaled == MathS.NaN) + return null; + return new Signumf(argument); + } } partial record Absf diff --git a/Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs b/Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs new file mode 100644 index 000000000..ef1affb67 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) 2019-2022 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using AngouriMath; +using AngouriMath.Core; +using AngouriMath.Extensions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// The limit of a signum. It had no reading of its own and handed back an unevaluated limit + /// of the very expression it was asked about, which did not merely fail to answer: the + /// two-sided path compares its two one-sided results by evaluating them, evaluating a limit + /// computes it, and computing it arrived back at the same place. The recursion ended by + /// overflowing the stack, which kills the process rather than raising anything a caller + /// could catch -- https://github.com/asc-community/AngouriMath/issues/704. + /// + public sealed class SignumLimitTest + { + private static Entity Limit(string expression, string destination) => + expression.ToEntity().Limit("x", destination.ToEntity()).Simplify(); + + /// + /// Away from zero the sign is constant, so the limit is the sign of whatever the + /// argument tends to -- including at the infinities, where it is 1 and -1. + /// + [Theory] + [InlineData("signum(x)", "2", "1")] + [InlineData("signum(x)", "-2", "-1")] + [InlineData("signum(x - 5)", "2", "-1")] + [InlineData("signum(x ^ 2 + 1)", "0", "1")] + [InlineData("signum(x)", "+oo", "1")] + [InlineData("signum(x)", "-oo", "-1")] + public void TheSignOfWhereTheArgumentGoes(string expression, string destination, string expected) => + Assert.Equal(expected.ToEntity().Evaled, Limit(expression, destination).Evaled); + + /// + /// Where the argument tends to zero there is nothing to say: the sign is 1 on one side + /// and -1 on the other, and which one is taken depends on the direction the argument + /// approaches from rather than only on what it tends to. Left unevaluated -- and, above + /// all, terminating, which is what this test is really for. + /// + [Theory] + [InlineData("signum(x)", "0")] + [InlineData("signum(x ^ 3)", "0")] + public void AtZeroItIsLeftUnevaluatedAndTerminates(string expression, string destination) + { + var task = Task.Run(() => Limit(expression, destination)); + Assert.True(task.Wait(TimeSpan.FromSeconds(30)), "the limit did not terminate"); + Assert.IsType(task.Result); + } + + [Theory] + [InlineData(ApproachFrom.Left)] + [InlineData(ApproachFrom.Right)] + public void OneSidedAtZeroTerminatesToo(ApproachFrom side) + { + var task = Task.Run(() => "signum(x)".ToEntity().Limit("x", 0, side).Simplify()); + Assert.True(task.Wait(TimeSpan.FromSeconds(30)), "the limit did not terminate"); + Assert.IsType(task.Result); + } + + /// + /// A signum inside a larger expression must not drag the whole limit down with it. + /// + [Theory] + [InlineData("signum(x) * x", "2", "2")] + [InlineData("signum(x) + 1", "3", "2")] + [InlineData("abs(x)", "2", "2")] + public void ItComposes(string expression, string destination, string expected) => + Assert.Equal(expected.ToEntity().Evaled, Limit(expression, destination).Evaled); + } +}