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)