diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 4958f4e35..48ee87fc0 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -245,6 +245,22 @@ is the canonical one: square-free, and without what the answer's own denominator | `"(a - 1)/(1 - b) - (1 - a)/(b - 1)".Simplify()` | `0 provided not (b - 1) * (1 - b) = 0` | `0 provided not b - 1 = 0` | | `"(x*y*a*b*c)/(c*b*a*x*x)".Simplify()` | `y / x provided not a * b * c * x = 0` | `y / x provided not a * b * c = 0` | +### A step function at a jump takes the side its argument arrives from + +**Silent: a limit of `floor`, `ceil` or `sgn` whose argument arrives on a jump is answered, where it was left +unevaluated.** A one-sided limit is the value on the side the argument arrives from, which is the sign the argument less +the jump keeps near the destination, and not always the side x comes from: `floor(2 - x^2)` comes to 2 from below as x comes +to 0 from the right, and its limit there is 1. A two-sided limit at a jump has two sides that differ, and is `NaN`, which +says that there is none ([#2266](https://github.com/asc-community/AngouriMath/issues/2266)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"limitleft(floor(x), x, 0)".ToEntity().Simplify()` | `limitleft(floor(x), x, 0)` | `-1` | +| `"limitright(floor(2 - x^2), x, 0)".ToEntity().Simplify()` | `limitright(floor(2 - x ^ 2), x, 0)` | `1` | +| `"limitright(sgn(x^3 - x), x, 0)".ToEntity().Simplify()` | `limitright(sgn(x ^ 3 - x), x, 0)` | `-1` | +| `"limit(floor(x), x, 1)".ToEntity().Simplify()` | `limit(floor(x), x, 1)` | `NaN` | +| `"limit(sgn(x), x, 0)".ToEntity().Simplify()` | `limit(sgn(x), x, 0)` | `NaN` | + ### A polynomial system is not answered with some of its solutions missing **Silent: a polynomial system that lost solutions has all of them.** `EquationSystem.Solve` triangularises a diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 96598d54f..07703cf42 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -120,6 +120,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Calculus/ProductOfPowersDerivativeTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[Tests/UnitTests/Calculus/StepFunctionAtAJumpTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [Tests/UnitTests/Calculus/LogarithmOfARationalFunctionByPartsTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs index d4dba0e85..ca11d7fdd 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs @@ -410,21 +410,27 @@ partial record Signumf 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. + // infinities, where it is 1 and -1. At zero 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 + // Where that direction cannot be read, nothing is returned 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) + if (argument.Evaled is not Number value || value.Evaled == MathS.NaN) return null; + // At zero the side the argument arrives from is the sign, where it can be read: + // sgn(x^3 - x) at 0+ is -1, from below. A two-sided limit is its two sides, each + // asked of this, and is not answered here. + // https://github.com/asc-community/AngouriMath/issues/2266 + if (value == 0) + return SignNear(Argument, x, dist, side) is { } arrivesFrom ? Integer.Create(arrivesFrom) : null; return new Signumf(argument); } } @@ -441,14 +447,19 @@ partial record Absf /// /// /// Away from the jumps the function is locally constant, so the limit is simply the - /// function of the argument's limit. On a jump there is nothing to say: the value - /// differs on the two sides of it, and which side the argument arrives from is not - /// decided by the side approaches its destination from -- - /// lim(x -> 0+) floor(2 - x^2) reaches 2 from below and lim(x -> 0+) floor(2 + x^2) - /// from above, and both are limits from the right. + /// function of the argument's limit. On a jump the value differs on the two sides of it, + /// and which side the argument arrives from is not decided by the side + /// approaches its destination from -- lim(x -> 0+) floor(2 - x^2) + /// reaches 2 from below and lim(x -> 0+) floor(2 + x^2) from above, and both are limits + /// from the right. It is the sign the argument less the jump keeps on that side, which + /// reads off the first derivative that does not vanish, and + /// gives the value from it. A two-sided limit is asked of + /// each side, where the sign is not one sign, and is not answered here. + /// #2266 /// - /// Null is returned there rather than an unevaluated limit of the very expression being - /// asked about. The latter is what the inherited default does, and it does not merely + /// Null is returned where the sign cannot be read, rather than an unevaluated limit of + /// the very expression being asked about. The latter is what the inherited default does, + /// and it does not merely /// fail to answer: the two-sided path compares its 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 @@ -459,7 +470,7 @@ partial record Absf /// same one on . /// private static Entity? LimitOfAStepFunction(Entity argument, Variable x, Entity dist, - ApproachFrom side, System.Func rebuild) + ApproachFrom side, System.Func rebuild, System.Func atAJump) { if (argument.ComputeLimitDivideEtImpera(x, dist, side) is not { } limit) return null; @@ -482,7 +493,9 @@ partial record Absf if (value is not Complex { RealPart: var real, ImaginaryPart: var imaginary }) return null; if (SitsOnAJump(real)) - return null; + return value is Integer jump && SignNear(argument - jump, x, dist, side) is { } arrivesFrom + ? atAJump(jump, arrivesFrom) + : null; if (value is not Real && SitsOnAJump(imaginary)) return null; return rebuild(limit); @@ -494,13 +507,15 @@ static bool SitsOnAJump(Real part) => partial record Floorf { internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) - => LimitOfAStepFunction(Argument, x, dist, side, static a => new Floorf(a)); + => LimitOfAStepFunction(Argument, x, dist, side, static a => new Floorf(a), + static (jump, arrivesFrom) => arrivesFrom > 0 ? jump : jump - 1); } partial record Ceilf { internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) - => LimitOfAStepFunction(Argument, x, dist, side, static a => new Ceilf(a)); + => LimitOfAStepFunction(Argument, x, dist, side, static a => new Ceilf(a), + static (jump, arrivesFrom) => arrivesFrom > 0 ? jump + 1 : jump); } partial record Providedf diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs index 46d3584a0..0607d390c 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs @@ -1037,7 +1037,7 @@ internal static bool IsDeterminate(Entity combined, Variable x) /// The sign an expression keeps throughout a punctured one-sided neighbourhood of the /// destination, or where it cannot be read off. /// - private static int? SignNear(Entity expr, Variable x, Entity dest, ApproachFrom side) + internal static int? SignNear(Entity expr, Variable x, Entity dest, ApproachFrom side) { if (EvalAssumingContinuous(expr.Limit(x, dest, side)) is not Real { IsNaN: false } limit) return null; diff --git a/Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs b/Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs index df9bc0a7a..1136b3a65 100644 --- a/Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs +++ b/Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs @@ -43,29 +43,33 @@ public void TheSignOfWhereTheArgumentGoes(string expression, string destination, 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 + /// Where the argument tends to zero the sign is 1 on one side and -1 on the other, so + /// there is no two-sided limit, and NaN says so -- it was left unevaluated before the + /// sides were read (https://github.com/asc-community/AngouriMath/issues/2266). 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) + public void AtZeroThereIsNoLimitAndItTerminates(string expression, string destination) { var task = Task.Run(() => Limit(expression, destination)); Assert.True(task.Wait(LimitTermination.Guard), "the limit did not terminate"); - Assert.IsType(task.Result); + Assert.Equal(MathS.NaN, task.Result); } + /// + /// Which one a one-sided limit takes depends on the direction the argument approaches + /// from, which here is the direction x does. Terminating as the two-sided one does. + /// [Theory] - [InlineData(ApproachFrom.Left)] - [InlineData(ApproachFrom.Right)] - public void OneSidedAtZeroTerminatesToo(ApproachFrom side) + [InlineData(ApproachFrom.Left, -1)] + [InlineData(ApproachFrom.Right, 1)] + public void OneSidedAtZeroIsTheSignOfItsSide(ApproachFrom side, int sign) { var task = Task.Run(() => "signum(x)".ToEntity().Limit("x", 0, side).Simplify()); Assert.True(task.Wait(LimitTermination.Guard), "the limit did not terminate"); - Assert.IsType(task.Result); + Assert.Equal(Entity.Number.Integer.Create(sign), task.Result); } /// diff --git a/Sources/Tests/UnitTests/Calculus/StepFunctionAtAJumpTest.cs b/Sources/Tests/UnitTests/Calculus/StepFunctionAtAJumpTest.cs new file mode 100644 index 000000000..e84b025d6 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/StepFunctionAtAJumpTest.cs @@ -0,0 +1,43 @@ +// +// 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 AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A one-sided limit of floor, ceil or sgn whose argument arrives on a jump is the value on the side the argument arrives + /// from, where it was left unevaluated. That side is the sign the argument less the jump keeps near the destination, and + /// is not the side x comes from: floor(2 - x^2) comes to 2 from below as x comes to 0 from the right. A two-sided limit at + /// a jump has two sides that differ, and is NaN. + /// #2266 + /// + [Trait("Area", "Calculus")] + public sealed class StepFunctionAtAJumpTest + { + [Theory] + [InlineData("limitleft(floor(x), x, 0)", "-1")] + [InlineData("limitright(floor(x), x, 0)", "0")] + [InlineData("limitright(floor(2 - x^2), x, 0)", "1")] + [InlineData("limitright(floor(2 + x^2), x, 0)", "2")] + [InlineData("limitright(floor(-x), x, 0)", "-1")] + [InlineData("limitleft(floor(cos(x)), x, 0)", "0")] + [InlineData("limitleft(ceil(x), x, 0)", "0")] + [InlineData("limitright(ceil(x), x, 0)", "1")] + [InlineData("limitright(sgn(x), x, 0)", "1")] + [InlineData("limitleft(sgn(x), x, 0)", "-1")] + [InlineData("limitright(sgn(x^3 - x), x, 0)", "-1")] + [InlineData("limit(sgn(x^2), x, 0)", "1")] + [InlineData("limit(floor(x^2), x, 0)", "0")] + [InlineData("limit(sgn(x), x, 0)", "NaN")] + [InlineData("limit(floor(x), x, 1)", "NaN")] + [InlineData("limit(floor(x), x, 1/2)", "0")] + public void ItIsTheValueOnTheSideTheArgumentArrivesFrom(string limit, string value) + => Assert.Equal(value.ToEntity().Evaled, limit.ToEntity().Simplify().Evaled); + } +} diff --git a/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs b/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs index dc56c0a89..e1bcac041 100644 --- a/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs +++ b/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs @@ -214,10 +214,10 @@ public void TheLimitAwayFromAJumpIsTheValue(string input, string destination, st input.ToEntity().Limit("x", destination.ToEntity()).Evaled); /// - /// On a jump the two sides disagree, and which side the argument arrives from is not - /// decided by the side x approaches its destination from. So the answer is an - /// unevaluated limit — the same thing signum returns at zero. What it must not - /// do is pick one of the two values. + /// On a jump the two sides disagree, so there is no two-sided limit, and the answer is + /// NaN -- the same thing signum returns at zero. It was an unevaluated limit + /// before the sides were read (https://github.com/asc-community/AngouriMath/issues/2266). + /// What it must not do is pick one of the two values. /// [Theory] [InlineData("floor(x)", "0")] @@ -225,9 +225,9 @@ public void TheLimitAwayFromAJumpIsTheValue(string input, string destination, st [InlineData("floor(x)", "-3")] [InlineData("ceil(x)", "0")] [InlineData("ceil(x)", "2")] - public void TheLimitOnAJumpIsDeclined(string input, string destination) - => Assert.IsType( - input.ToEntity().Limit("x", destination.ToEntity())); + public void TheLimitOnAJumpDoesNotExist(string input, string destination) + => Assert.Equal(MathS.NaN, + input.ToEntity().Limit("x", destination.ToEntity()).Evaled); /// /// A rational with more digits than the precision, a third away from an integer on either