diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 1090e0192..f12988228 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -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
diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs
index ac169479f..e074aa215 100644
--- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs
@@ -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);
}
}
diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs
index 0fa5b956c..d3a77ec78 100644
--- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs
@@ -96,8 +96,8 @@ private static bool IsComplexJustShortOfInfinity(Entity argument, Variable x)
&& LimitFunctional.AtALargeDecimal(argument, x) is Complex and not Real;
///
- /// Whether is a step function whose argument evaluates to one of its jumps: an integer for
- /// floor and ceil, a half-integer for round, 0 for sgn. Taken on the real part, and on the
+ /// Whether is a step function, or an arccotangent, whose argument evaluates to one of its jumps: an integer for
+ /// floor and ceil, a half-integer for round, 0 for sgn and arccot. Taken on the real part, and on the
/// imaginary part of a value that is not real, as LimitOfAStepFunction takes it.
///
private static bool SitsOnAJump(Entity node)
@@ -112,6 +112,9 @@ static bool OnAJump(Entity argument, System.Func 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,
};
}
diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
index 8002c9a16..90072cd8f 100644
--- a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
@@ -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;
@@ -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);
+ ///
+ /// Whether holds an arccotangent whose argument is 0 at , 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.
+ ///
+ 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 });
+
///
/// The value of at a finite , where it is a finite real number and
/// nothing in it jumps, which is its limit there; otherwise .
///
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;
@@ -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;
@@ -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 })
diff --git a/Sources/Tests/UnitTests/Calculus/AnArccotangentAtItsJumpTest.cs b/Sources/Tests/UnitTests/Calculus/AnArccotangentAtItsJumpTest.cs
new file mode 100644
index 000000000..2ef9be4cb
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/AnArccotangentAtItsJumpTest.cs
@@ -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
+{
+ ///
+ /// 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.
+ ///
+ [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));
+ }
+}
diff --git a/Sources/Tests/UnitTests/Calculus/LimitTest.cs b/Sources/Tests/UnitTests/Calculus/LimitTest.cs
index cc54b65d9..aff0e57df 100644
--- a/Sources/Tests/UnitTests/Calculus/LimitTest.cs
+++ b/Sources/Tests/UnitTests/Calculus/LimitTest.cs
@@ -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)