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
25 changes: 25 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,28 @@ A not-equal written `<>` is read as `not (a = b)`, and printed that way.

## Unreleased — since 2.6.0

### A step function of an unbounded argument times a vanishing factor has the limit of the factor times the argument

**Silent: a limit of `f * floor(g)`, `f * ceil(g)` or `f * round(g)`, where `f` tends to 0 and `g` is real, is
answered with the limit of `f * g`.** The step is within 1 of its argument, so the two products differ by `f` times
something bounded, which tends to 0. Each side used to stop at `0 * floor(+oo)`, which 2.6.0 answered with a wrong `0`
one-sided and `NaN` two-sided. Where `f * g` has no limit of its own the limit is left as it was. Found by limitcheck;
no issue was filed.

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"x * floor(2/x)".ToEntity().Limit("x", 0)` | `NaN` | `2` |
| `"x * floor(2/x)".ToEntity().Limit("x", 0, ApproachFrom.Right)` | `0` | `2` |
| `"floor(1/x) * sin(x)".ToEntity().Limit("x", 0)` | `NaN` | `1` |
| `"x^2 * floor(1/x^2)".ToEntity().Limit("x", 0)` | `0` | `1` |
| `"x * ceil(1/x)".ToEntity().Limit("x", 0)` | `NaN` | `1` |
| `"x * round(1/x)".ToEntity().Limit("x", 0)` | throws `OverflowException` | `1` |
| `"x^2 * floor(1/x)".ToEntity().Limit("x", 0)` | `NaN` | `0` |
| `"x * floor(1/x^2)".ToEntity().Limit("x", 0, ApproachFrom.Right)` | `0` | `+oo` |
| `"x * floor(1/x^2)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `0` | `-oo` |
| `"x * floor(2/x) + 3".ToEntity().Limit("x", 0)` | `limit(x * floor(2 / x) + 3, x, 0)` | `5` |
| `"floor(x) / x".ToEntity().Limit("x", "+oo")` | `0` | `1` |

### A product with a quotient for a factor is read over that quotient's divisor in a one-sided limit

**Silent: a one-sided limit of a product whose factors tend to 0 and to an infinity, one of them a quotient, is asked
Expand Down Expand Up @@ -101,6 +123,9 @@ and 1 in order. 2.6.0 answered the third with a wrong `0`. Found by limitcheck;
| `"x^2 * floor(1/x^2)".ToEntity().Limit("x", 0)` | `0` | `limit(x ^ 2 * floor(1 / x ^ 2), x, 0)` |
| `"x * ceil(1/x)".ToEntity().Limit("x", 0)` | `NaN` | `limit(x * ceil(1 / x), x, 0)` |

These four are answered with their values by the entry *A step function of an unbounded argument times a vanishing
factor has the limit of the factor times the argument*, above.

### A two-sided limit whose side stops at a finite indeterminate form is left unevaluated

**Silent: a two-sided limit is left unevaluated, rather than answered, where a side's limit stops at an indeterminate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,47 @@ static bool OnAJump(Entity argument, System.Func<Real, bool> jump)
? Real.NaN
: null;

/// <summary>
/// A step function of an unbounded argument times a factor that vanishes: the limit of
/// <c>f * floor(g)</c> is the limit of <c>f * g</c>, and the same for <c>ceil</c> and
/// <c>round</c>.
/// </summary>
/// <remarks>
/// <c>floor(g) = g - r</c> with <c>0 &lt;= r &lt; 1</c> wherever <c>g</c> is real, <c>ceil(g) = g + r</c>
/// with the same bound, and <c>round(g)</c> is within 1/2 of <c>g</c>. So
/// <c>f * floor(g) - f * g</c> is <c>f</c> times something bounded by 1, which tends to 0
/// wherever <c>f</c> does, and the two products have the same limit, a value or an
/// infinity alike. Assumed, and checked: <c>g</c> is real-valued, and the other factors
/// tend to 0. Nothing is assumed of <c>g</c>'s limit, which is the point -- the descent
/// puts each part's own limit in place of the part, so <c>x floor(2/x)</c> at 0 was read
/// as <c>0 * floor(+oo)</c>, a form it cannot settle, where it is 2 from either side.
/// <para/>
/// Only a limit is taken from it. A <c>NaN</c> for <c>f * g</c> would say the same of the
/// step, but inside the machinery <c>NaN</c> can also be a form nothing read, so it is left
/// to the rules that already decide those.
/// </remarks>
internal static Entity? SolveAsAStepOfAnUnboundedArgument(Entity expr, Variable x)
{
if (expr is not (Mulf or Divf) || !expr.Nodes.Any(static node => node is Floorf or Ceilf or Roundf))
return null;
var factors = Mulf.LinearChildren(expr);
for (var i = 0; i < factors.Count; i++)
{
if (factors[i] is not (Floorf or Ceilf or Roundf) || !factors[i].ContainsNode(x))
continue;
var argument = factors[i].DirectChildren[0];
if (!IsRealValued(argument, x))
continue;
var rest = factors.Where((_, j) => j != i).Aggregate((Entity)Integer.One, static (product, factor) => product * factor);
if (!rest.ContainsNode(x) || !VanishesAtInfinity(rest, x))
continue;
if (LimitFunctional.ComputeLimit((rest * argument).InnerSimplified, x, Real.PositiveInfinity) is { } limit
&& limit.Evaled != MathS.NaN)
return limit;
}
return null;
}

private static bool VanishesAtInfinity(Entity expr, Variable x)
=> LimitFunctional.ComputeLimit(expr, x, Real.PositiveInfinity) is { } limit
&& limit.Evaled is Real { IsZero: true };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ private static Entity AsAPowerOfTheVariable(Entity node, Variable x)
var oscillationResult = LimitSolvers.SolveAsOscillationWithoutLimit(expr, x);
if (oscillationResult is { }) return oscillationResult;

// After both rules above, which settle the products they read without asking for
// another limit; this one asks for the limit of a second product.
var stepResult = LimitSolvers.SolveAsAStepOfAnUnboundedArgument(expr, x);
if (stepResult is { }) return stepResult;

return null;
}

Expand Down Expand Up @@ -184,8 +189,8 @@ node is Floorf or Ceilf or Roundf or Signumf
{
// Set where no answer came because a side has none yet, rather than because a side
// does not exist or the two differ. Neither side is then known to be NaN, so neither
// is the limit: x floor(2/x) at 0 is 2 from each side, the one-sided paths do not
// decide it, and the two-sided one answered NaN.
// is the limit: floor(1/x) / floor(2/x) at 0 is 1/2 from each side, the one-sided
// paths do not decide it, and a NaN here would say there is no limit.
aSideIsUndecided = false;
if (bothSidesDepth >= MaxBothSidesDepth
|| MathS.Settings.Codomain.Value is not AngouriMath.Core.Domain.Real && !IsRealOnBothSides(expr, x, dest))
Expand Down Expand Up @@ -213,7 +218,7 @@ node is Floorf or Ceilf or Roundf or Signumf

// An indeterminate form a side's own limit could not read is NaN inside the machinery,
// where it stops the search, and withheld at the outermost call, where it is not an
// answer. One with an infinity in it, 0 * floor(-oo) for x floor(2/x), is a product or
// answer. One with an infinity in it, such as 0 * oo or oo / oo, is a product or
// a quotient of parts whose rates were never compared, so that side is undecided rather
// than absent. So is one of finite parts where the expression takes real values on that
// side: erf(abs(x)) / x^4 at 0 stops at 0 / 0 from each side, is +oo from each, and was
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// 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.Core;
using AngouriMath.Extensions;
using Xunit;

namespace AngouriMath.Tests.Calculus
{
/// <summary>
/// f floor(g), f ceil(g) and f round(g), where f tends to 0 and g is real, have the limit of f g: the step is within 1
/// of its argument, so the two products differ by f times something bounded. x floor(2/x) at 0 was left unevaluated,
/// each side stopping at 0 * floor(+-oo), where it is 2. Each value here was checked at x = +-7/3 * 10^-k for k from 4
/// to 8. No issue was filed.
/// </summary>
[Trait("Area", "Calculus")]
public sealed class AStepOfAnUnboundedArgumentTimesAVanishingFactorTest
{
[Theory]
[InlineData("x * floor(2/x)", "0", "2")]
[InlineData("floor(1/x) * sin(x)", "0", "1")]
[InlineData("x^2 * floor(1/x^2)", "0", "1")]
[InlineData("x * ceil(1/x)", "0", "1")]
[InlineData("x * round(1/x)", "0", "1")]
[InlineData("x * ceil(-2/x)", "0", "-2")]
[InlineData("-3 * x * floor(1/x)", "0", "-3")]
[InlineData("x^2 * floor(1/x)", "0", "0")]
[InlineData("(x - 1) * floor(1/(x - 1))", "1", "1")]
[InlineData("x * floor(2/x) + 3", "0", "5")]
[InlineData("floor(x) / x", "+oo", "1")]
public void FromEachSideAndBoth(string input, string destination, string expected)
{
var expr = input.ToEntity();
Assert.Equal(expected.ToEntity(), expr.Limit("x", destination, ApproachFrom.Left).InnerSimplified);
Assert.Equal(expected.ToEntity(), expr.Limit("x", destination, ApproachFrom.Right).InnerSimplified);
Assert.Equal(expected.ToEntity(), expr.Limit("x", destination).InnerSimplified);
}

// Where f g has no limit, neither has f floor(g): the sides diverge in different directions, so there is none.
[Theory]
[InlineData("x * floor(1/x^2)")]
[InlineData("sqrt(abs(x)) * floor(1/x)")]
public void SidesThatDivergeApart(string input)
{
var expr = input.ToEntity();
Assert.Equal(Entity.Number.Real.NegativeInfinity, expr.Limit("x", 0, ApproachFrom.Left));
Assert.Equal(Entity.Number.Real.PositiveInfinity, expr.Limit("x", 0, ApproachFrom.Right));
Assert.Equal(MathS.NaN, expr.Limit("x", 0));
}

// x floor(sin(1/x)/x) is sin(1/x) up to a term that vanishes, and has no limit. The rule takes only a value from
// the limit of f g, so it leaves this unevaluated rather than claiming anything.
[Fact]
public void AnOscillatingProductIsNotAnswered()
=> Assert.IsType<Entity.Limitf>("x * floor(sin(1/x)/x)".ToEntity().Limit("x", 0));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ namespace AngouriMath.Tests.Calculus
public sealed class AnUndecidedSideLeavesTheLimitUnevaluatedTest
{
[Theory]
[InlineData("x * floor(2/x)")]
[InlineData("floor(1/x) * sin(x)")]
[InlineData("x^2 * floor(1/x^2)")]
[InlineData("x * ceil(1/x)")]
// 1/2 from each side. The four this was written for, x floor(2/x) among them, are answered now, and are pinned
// in AStepOfAnUnboundedArgumentTimesAVanishingFactorTest.
[InlineData("floor(1/x) / floor(2/x)")]
// A side that stops at 0 / 0, or at c / 0, of an expression real beside the point is undecided too:
// erf(abs(x)) / x^4 is +oo from each side, 1 / log(x^2, cos(x)) is +oo, abs(x)^erf(x) is 1.
[InlineData("erf(abs(x)) / x^4")]
Expand Down
Loading