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
27 changes: 27 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,33 @@ A bare `floor(x)` or `sgn(x)` was already declined there. Away from a jump, noth
| `"limit(floor(x) + x, x, 1)".ToEntity().Simplify()` | `2` | `limit(floor(x) + x, x, 1)` |
| `"limitleft(-sgn(x), x, 0)".ToEntity().Simplify()` | `0` | `limitleft(-sgn(x), x, 0)` |

### An arccotangent whose argument arrives at 0 is answered from the side it arrives from

**Silent: a limit through `arccot(u)` where `u` tends to 0 takes `pi/2` where `u` arrives from above and `-pi/2` where
it arrives from below.** This library's arccotangent is `arctan(1/u)`, with range `(-pi/2, pi/2]`, so it jumps at 0:
`arccot(-0.001)` is `-1.5698` and `arccot(0)` is `pi/2`. Limits put 0 in and answered `pi/2` from either side, so a
two-sided limit across the jump had a value, and a quotient over a divisor vanishing from below had the wrong sign. The
sign of an expression vanishing at the point was read from its derivative there, and where that derivative holds
`arccot(x)` the sign came out wrong from the left: `x * arccot(x)` is positive on both sides of 0.
Found by limitcheck; no issue was filed.

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"arccot(x)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `pi / 2` | `-pi / 2` |
| `"arccot(x)".ToEntity().Limit("x", 0)` | `pi / 2` | `NaN` |
| `"arccot(-x^2)".ToEntity().Limit("x", 0)` | `pi / 2` | `-pi / 2` |
| `"arccot(x) + arctan(x)".ToEntity().Limit("x", 0)` | `pi / 2` | `NaN` |
| `"arccot(x) / x".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `-oo` | `+oo` |
| `"arccot(x) / x".ToEntity().Limit("x", 0)` | `NaN` | `+oo` |
| `"x * arccot(x) / sin(x)^2".ToEntity().Limit("x", 0)` | `NaN` | `+oo` |
| `"(arccot(x) - pi/2) / x".ToEntity().Limit("x", 0)` | `-1` | `NaN` |
| `"(arccot(x) + pi/2) / x".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `-oo` | `-1` |
| `"1 / (x * arccot(x))".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `-oo` | `limitleft(1 / (x * arccotan(x)), x, 0)` |
| `"sgn(x * arccot(x))".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `limitleft(sgn(x * arccotan(x)), x, 0)` | `1` |
| `"sgn(x * arccot(x))".ToEntity().Limit("x", 0)` | `limit(sgn(x * arccotan(x)), x, 0)` | `1` |
| `"floor(x * arccot(x))".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `limitleft(floor(x * arccotan(x)), x, 0)` | `0` |
| `"arccot(x * sin(1/x))".ToEntity().Limit("x", 0)` | `pi / 2` | `limit(arccotan(x * sin(1 / x)), x, 0)` |

### A step function arriving at 0 is answered from the side its argument arrives from

**Silent: a limit of `floor` or `ceil` whose argument tends to 0 is answered where the argument keeps one sign on the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -766,11 +766,31 @@ partial record Arccotanf
{
// A diverging argument first, as for the arcsecant: substituting it leaves the
// function of an infinity rather than the angle it tends to.
if (InverseTrigonometryAtInfinity(this, Argument.ComputeLimitDivideEtImpera(x, dist, side)) is { } atInfinity)
var argumentLimit = Argument.ComputeLimitDivideEtImpera(x, dist, side);
if (InverseTrigonometryAtInfinity(this, argumentLimit) is { } atInfinity)
return atInfinity;
// At 0 the arccotangent jumps: it is arctan(1/t), with range (-pi/2, pi/2], so
// arccot(0.001) = 1.5698, arccot(-0.001) = -1.5698 and arccot(0) = pi/2. A
// one-sided limit there is taken from the side the argument arrives from, as for
// the sign, and nothing is answered where that side cannot be read. Putting 0 in
// gave pi/2 for arccot(x) at 0 from the left, and for the two-sided limit, which
// is answered here only where both sides arrive from the same side of 0.
if (argumentLimit?.Evaled is Number argumentValue && argumentValue == 0)
{
var arrivesFrom = side is ApproachFrom.BothSides
? SignNear(Argument, x, dist, ApproachFrom.Left) is { } left && SignNear(Argument, x, dist, ApproachFrom.Right) == left ? left : null
: SignNear(Argument, x, dist, side);
return arrivesFrom switch
{
1 => MathS.pi / 2,
-1 => -MathS.pi / 2,
// An argument that is 0 everywhere has no side to arrive from, and the
// arccotangent of it is the constant pi/2: (x^2 - 1) - (x - 1)(x + 1).
_ => Argument.Simplify().Evaled is Number simplified && simplified == 0 ? MathS.pi / 2 : null
};
}
return ComputeLimitImpl(this, x, dist, side) is { } lim ? lim
: ComputeLimitImpl(New(
Argument.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim1 ? lim1 : Argument),
: ComputeLimitImpl(New(argumentLimit is { IsFinite: true } lim1 ? lim1 : Argument),
x, dist, side);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ private static bool IsComplexJustShortOfInfinity(Entity argument, Variable x)
&& LimitFunctional.AtALargeDecimal(argument, x) is Complex and not Real;

/// <summary>
/// Whether <paramref name="node"/> is a step function whose argument evaluates to one of its jumps: an integer for
/// <c>floor</c> and <c>ceil</c>, a half-integer for <c>round</c>, 0 for <c>sgn</c>. Taken on the real part, and on the
/// Whether <paramref name="node"/> is a step function, or an arccotangent, whose argument evaluates to one of its jumps: an integer for
/// <c>floor</c> and <c>ceil</c>, a half-integer for <c>round</c>, 0 for <c>sgn</c> and <c>arccot</c>. Taken on the real part, and on the
/// imaginary part of a value that is not real, as LimitOfAStepFunction takes it.
/// </summary>
private static bool SitsOnAJump(Entity node)
Expand All @@ -112,6 +112,9 @@ static bool OnAJump(Entity argument, System.Func<Real, bool> jump)
Ceilf(var argument) => OnAJump(argument, Whole),
Roundf(var argument) => OnAJump(argument, part => Whole((Real)(part + Rational.Create(1, 2)).Evaled)),
Signumf(var argument) => argument.Evaled is Complex { IsZero: true },
// The arccotangent is arctan(1/t), with range (-pi/2, pi/2], and jumps at 0 from -pi/2 to pi/2:
// arccot(-0.001) = -1.5698, arccot(0) = pi/2. Its override answers it from the side.
Arccotanf(var argument) => argument.Evaled is Complex { IsZero: true },
_ => false,
};
}
Expand Down
21 changes: 19 additions & 2 deletions Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,11 @@ internal static bool IsDeterminate(Entity combined, Variable x)
{
MultithreadingFunctional.ExitIfCancelled();
derivative = derivative.Differentiate(x).Simplify();
// A derivative with an arccotangent at its jump in it has no single value at the point:
// x arccot(x) differentiates to arccot(x) - x / (1 + x^2), pi/2 there by putting 0 in
// and -pi/2 from the left, so its sign from the left came out -1 where it is +1.
if (HasAnArccotangentAtItsJump(derivative, x, dest))
return null;
var atThePoint = EvalAssumingContinuous(derivative.Substitute(x, dest).InnerSimplified);
if (atThePoint is not Real { IsFinite: true } value)
return null;
Expand Down Expand Up @@ -1494,13 +1499,23 @@ internal static bool VanishesNear(Entity difference, Variable x, Entity dest, Ap
private static bool AlreadyBeingDifferentiated(Entity quotient)
=> (lHopitalChain ??= new()).Contains(quotient);

/// <summary>
/// Whether <paramref name="expr"/> holds an arccotangent whose argument is 0 at <paramref name="dest"/>, where it
/// jumps: it is arctan(1/t), with range (-pi/2, pi/2], so arccot(-0.001) = -1.5698 and arccot(0) = pi/2. Its value
/// there is not its limit from below, as a step function's value at a jump is not.
/// </summary>
private static bool HasAnArccotangentAtItsJump(Entity expr, Variable x, Entity dest)
=> expr.Nodes.Any(node => node is Arccotanf(var argument) && argument.ContainsNode(x)
&& argument.Substitute(x, dest).Evaled is Complex { IsZero: true });

/// <summary>
/// The value of <paramref name="expr"/> at a finite <paramref name="dest"/>, where it is a finite real number and
/// nothing in it jumps, which is its limit there; otherwise <see langword="null"/>.
/// </summary>
private static Entity? ItsValueAt(Entity expr, Variable x, Entity dest)
{
if (!dest.IsFinite || expr.Nodes.Any(static node => node is Floorf or Ceilf or Roundf or Signumf or Piecewise))
if (!dest.IsFinite || expr.Nodes.Any(static node => node is Floorf or Ceilf or Roundf or Signumf or Piecewise)
|| HasAnArccotangentAtItsJump(expr, x, dest))
return null;
var there = expr.Substitute(x, dest);
return there.Evaled is Real { IsFinite: true, IsNaN: false } ? there.InnerSimplified : null;
Expand All @@ -1523,7 +1538,8 @@ private static bool AlreadyBeingDifferentiated(Entity quotient)
private static Entity? ItsValueOrPoleAt(Entity applied, Variable x, Entity dest, ApproachFrom side)
{
if (!dest.IsFinite || side is ApproachFrom.BothSides
|| applied.Nodes.Any(static node => node is Floorf or Ceilf or Roundf or Signumf or Piecewise))
|| applied.Nodes.Any(static node => node is Floorf or Ceilf or Roundf or Signumf or Piecewise)
|| HasAnArccotangentAtItsJump(applied, x, dest))
return null;
var distance = dest is Integer { IsZero: true } ? (Entity)x : x - dest;
Entity finite = Integer.Zero;
Expand Down Expand Up @@ -2041,6 +2057,7 @@ bool TakeOut(Entity factor)
{
if (factor.ContainsNode(x)
&& !factor.Nodes.Any(static node => node is Floorf or Ceilf or Roundf or Signumf or Piecewise)
&& !(dest.IsFinite && HasAnArccotangentAtItsJump(factor, x, dest))
&& TreeAnalyzer.IsRealValued(factor, x)
&& (dest.IsFinite ? factor.Substitute(x, dest) : IsAPolynomialPower(factor, x) ? MathS.NaN : ComputeLimit(factor, x, dest, side) ?? MathS.NaN) is var there
&& there.Evaled is Real { IsFinite: true, IsNaN: false, IsZero: false })
Expand Down
55 changes: 55 additions & 0 deletions Sources/Tests/UnitTests/Calculus/AnArccotangentAtItsJumpTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// 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>
/// The arccotangent here is arctan(1/u), with range (-pi/2, pi/2]: arccot(-0.001) = -1.5698, arccot(0) = pi/2,
/// arccot(0.001) = 1.5698. It jumps at 0, and limits put 0 in and answered pi/2 from either side, so arccot(x) at 0
/// had a two-sided limit and arccot(x) / x from the left was -oo where it is +oo. Found by limitcheck; no issue was
/// filed.
/// </summary>
[Trait("Area", "Calculus")]
public sealed class AnArccotangentAtItsJumpTest
{
[Theory]
[InlineData("arccot(x)", ApproachFrom.Left, "-pi / 2")]
[InlineData("arccot(x)", ApproachFrom.Right, "pi / 2")]
[InlineData("arccot(sin(x))", ApproachFrom.Left, "-pi / 2")]
[InlineData("arccot(-x^2)", ApproachFrom.BothSides, "-pi / 2")]
[InlineData("arccot(x^2)", ApproachFrom.BothSides, "pi / 2")]
[InlineData("arccot(x) / x", ApproachFrom.Left, "+oo")]
[InlineData("arccot(x) / x", ApproachFrom.BothSides, "+oo")]
[InlineData("x * arccot(x) / sin(x)^2", ApproachFrom.BothSides, "+oo")]
[InlineData("(arccot(x) - pi / 2) / x", ApproachFrom.Right, "-1")]
[InlineData("(arccot(x) - pi / 2) / x", ApproachFrom.Left, "+oo")]
[InlineData("(arccot(x) + pi / 2) / x", ApproachFrom.Left, "-1")]
// x arccot(x) is positive on both sides of 0: its derivative there holds arccot(x) itself,
// whose value at 0 is not its limit from the left, so it does not give the sign.
[InlineData("arccot(x * arccot(x))", ApproachFrom.Left, "pi / 2")]
[InlineData("arccot(x * arccot(x))", ApproachFrom.BothSides, "pi / 2")]
[InlineData("sgn(x * arccot(x))", ApproachFrom.Left, "1")]
[InlineData("1 / (x * arccot(x))", ApproachFrom.Left, "+oo")]
// An argument that is 0 everywhere: the arccotangent of it is the constant pi/2.
[InlineData("arccot((x^2 - 1) - (x - 1) * (x + 1))", ApproachFrom.BothSides, "pi / 2")]
public void IsTakenFromTheSide(string input, ApproachFrom side, string expected)
=> Assert.Equal(expected.Simplify(), input.ToEntity().Limit("x", 0, side).Simplify());

// Where the two sides arrive on different sides of the jump, there is no two-sided limit.
[Theory]
[InlineData("arccot(x)")]
[InlineData("arccot(sin(x))")]
[InlineData("arccot(x) + arctan(x)")]
[InlineData("(arccot(x) - pi / 2) / x")]
public void HasNoTwoSidedLimitAcrossTheJump(string input)
=> Assert.Equal(MathS.NaN, input.ToEntity().Limit("x", 0));
}
}
1 change: 0 additions & 1 deletion Sources/Tests/UnitTests/Calculus/LimitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ public void TestGeneral(string exprRaw, string destRaw, string expectedRaw, Appr
[InlineData("sec(x)", "1")]
// [InlineData("a x * csc(b x)", "a / b", Skip = "Equivalence table needs improvement")]
[InlineData("(arccos(x) - pi / 2) / x", "-1")]
[InlineData("(arccotan(x) - pi / 2) / x", "-1")]
[InlineData("arccsc(1 / x)", "0")]
[InlineData("arcsec(1 / x) - pi / 2", "0")]
public void TestEquivalenceTableTo0(string input, string expected)
Expand Down
Loading