Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand All @@ -441,14 +447,19 @@ partial record Absf
/// </summary>
/// <remarks>
/// 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 <paramref name="x"/> 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
/// <paramref name="x"/> 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
/// <see cref="SignNear"/> reads off the first derivative that does not vanish, and
/// <paramref name="atAJump"/> 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.
/// <a href="https://github.com/asc-community/AngouriMath/issues/2266">#2266</a>
/// <para/>
/// 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
Expand All @@ -459,7 +470,7 @@ partial record Absf
/// same one on <see cref="Signumf"/>.
/// </remarks>
private static Entity? LimitOfAStepFunction(Entity argument, Variable x, Entity dist,
ApproachFrom side, System.Func<Entity, Entity> rebuild)
ApproachFrom side, System.Func<Entity, Entity> rebuild, System.Func<Integer, int, Entity> atAJump)
{
if (argument.ComputeLimitDivideEtImpera(x, dist, side) is not { } limit)
return null;
Expand All @@ -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);
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <see langword="null"/> where it cannot be read off.
/// </summary>
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;
Expand Down
22 changes: 13 additions & 9 deletions Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,29 +43,33 @@ public void TheSignOfWhereTheArgumentGoes(string expression, string destination,
Assert.Equal(expected.ToEntity().Evaled, Limit(expression, destination).Evaled);

/// <summary>
/// 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.
/// </summary>
[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<Entity.Limitf>(task.Result);
Assert.Equal(MathS.NaN, task.Result);
}

/// <summary>
/// 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.
/// </summary>
[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<Entity.Limitf>(task.Result);
Assert.Equal(Entity.Number.Integer.Create(sign), task.Result);
}

/// <summary>
Expand Down
43 changes: 43 additions & 0 deletions Sources/Tests/UnitTests/Calculus/StepFunctionAtAJumpTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// <a href="https://github.com/asc-community/AngouriMath/issues/2266">#2266</a>
/// </summary>
[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);
}
}
14 changes: 7 additions & 7 deletions Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,20 +214,20 @@ public void TheLimitAwayFromAJumpIsTheValue(string input, string destination, st
input.ToEntity().Limit("x", destination.ToEntity()).Evaled);

/// <summary>
/// On a jump the two sides disagree, and which side the argument arrives from is not
/// decided by the side <c>x</c> approaches its destination from. So the answer is an
/// unevaluated limit — the same thing <c>signum</c> 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 <c>signum</c> 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.
/// </summary>
[Theory]
[InlineData("floor(x)", "0")]
[InlineData("floor(x)", "2")]
[InlineData("floor(x)", "-3")]
[InlineData("ceil(x)", "0")]
[InlineData("ceil(x)", "2")]
public void TheLimitOnAJumpIsDeclined(string input, string destination)
=> Assert.IsType<Entity.Limitf>(
input.ToEntity().Limit("x", destination.ToEntity()));
public void TheLimitOnAJumpDoesNotExist(string input, string destination)
=> Assert.Equal(MathS.NaN,
input.ToEntity().Limit("x", destination.ToEntity()).Evaled);

/// <summary>
/// A rational with more digits than the precision, a third away from an integer on either
Expand Down
Loading