diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 3d445b212..864a86844 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -93,6 +93,23 @@ root near 23. | `"x^7 - 5x^6 + 1 = 0".Solve("x")` | six roots, none of them near 5 | seven roots | | `"x^4 - 3*x^(5/2) + 2 = 0".Solve("x")` | `{ 1, 4.8870…^(2/5) }` | four roots: `1`, `1.3734…^2`, and two off the real line | +### A trigonometric function reads the value its argument tends to + +**A one-sided limit at a finite point of a trigonometric function is answered, where it was left unevaluated.** The +limit is taken by moving the variable out to infinity, and the value read off there kept the infinity in it: +`x^2 + x - 1` from the left at 0 came back as `-1 - 1/+oo + 1/+oo^2`. The function above it asked whether that was +finite, was told no, and declined. A trigonometric function's argument now has the infinities folded out of its limit. +A function with a cut, such as the arctangent or the logarithm, still declines there: it is not continuous on its cut, +and folding would answer with the value on it. + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"sin(x^2 + x - 1)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `limitleft(sin(x ^ 2 + x - 1), x, 0)` | `sin(-1)` | +| `"sin(x^2 + x - 1)".ToEntity().Limit("x", 0)` | `limit(sin(x ^ 2 + x - 1), x, 0)` | `sin(-1)` | +| `"cos(x^2 + x + 2)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `limitleft(cos(x ^ 2 + x + 2), x, 0)` | `cos(2)` | +| `"tan(x^2 + x + 2)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `limitleft(tan(x ^ 2 + x + 2), x, 0)`, in 4.3 s | `sin(2) / cos(2)` | +| `"tan(x^2 + x - 1)".ToEntity().Limit("x", 0)` | `limit(tan(x ^ 2 + x - 1), x, 0)` | `sin(-1) / cos(-1)` | + ### A quartic that is biquadratic once depressed is solved **Silent: a quartic whose depressed form `y^4 + alpha y^2 + gamma`, for `y = x + b/(4a)`, has no cubic term is solved, @@ -150,7 +167,7 @@ the left at -1, which is `-1 - 0.129 i`, and which 2.7.0's previous changes had |---|---|---| | `"tan(x^2 + x - 1)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `-cos(-1) / sin(-1)` | `tan(-1)` | | `"sin(x^2 + x - 1) / cos(x^2 + x - 1)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `-cos(-1) / sin(-1)` | `tan(-1)` | -| `"tan(x^2 - x - 1)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `-cos(-1) / sin(-1)` | `limitleft(tan(x ^ 2 - x - 1), x, 0)` | +| `"tan(x^2 - x - 1)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `-cos(-1) / sin(-1)` | `sin(-1) / cos(-1)`, with the entry below | | `"-x + ln(2*pi*x)/2".ToEntity().Limit("x", "+oo", ApproachFrom.Left)` | `limitleft(-x + ln(2 * pi * x) / 2, x, +oo)` | `-oo` | | `"x - sqrt(x^2 + x)".ToEntity().Limit("x", "+oo", ApproachFrom.Left)` | `limitleft(x - sqrt(x ^ 2 + x), x, +oo)` | `-1/2` | | `"e^x * x".ToEntity().Limit("x", "-oo", ApproachFrom.Right)` | `limitright(e ^ x * x, x, -oo)` | `0` | diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs index 3adb73524..e465e7f1f 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs @@ -193,12 +193,27 @@ partial record Modf } } + /// + /// A part's limit with the destination folded out of it. A one-sided limit at a finite + /// point moves the variable out to infinity, and the value read off there keeps the + /// infinity in it: x^2 + x - 1 from the left at 0 is -1 - 1/+oo + 1/+oo^2, which is -1 + /// but is not finite to the check above, so sin(x^2 + x - 1) from the left at 0 was left + /// unevaluated. Only for the trigonometric functions, which have no cut, and so are + /// continuous at every finite argument short of a pole. A function with a cut is not: + /// arctan and the logarithm decline there by their own reading, and folding their + /// argument's limit would answer with the value on the cut. + /// + private static Entity? WithoutTheDestination(Entity? limit) + => limit is { } found && found.Nodes.Any(node => node is Real { IsFinite: false }) + && found.InnerSimplified is var folded && !folded.Nodes.Any(node => node is Real { IsFinite: false }) + ? folded : limit; + partial record Sinf { internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) => ComputeLimitImpl(this, x, dist, side) is { } lim ? lim : ComputeLimitImpl(New( - Argument.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim1 ? lim1 : Argument), + WithoutTheDestination(Argument.ComputeLimitDivideEtImpera(x, dist, side)) is { IsFinite: true } lim1 ? lim1 : Argument), x, dist, side); } @@ -207,7 +222,7 @@ partial record Cosf internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) => ComputeLimitImpl(this, x, dist, side) is { } lim ? lim : ComputeLimitImpl(New( - Argument.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim1 ? lim1 : Argument), + WithoutTheDestination(Argument.ComputeLimitDivideEtImpera(x, dist, side)) is { IsFinite: true } lim1 ? lim1 : Argument), x, dist, side); } @@ -216,7 +231,7 @@ partial record Secantf internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) => ComputeLimitImpl(this, x, dist, side) is { } lim ? lim : ComputeLimitImpl(New( - Argument.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim1 ? lim1 : Argument), + WithoutTheDestination(Argument.ComputeLimitDivideEtImpera(x, dist, side)) is { IsFinite: true } lim1 ? lim1 : Argument), x, dist, side); } @@ -225,7 +240,7 @@ partial record Cosecantf internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) => ComputeLimitImpl(this, x, dist, side) is { } lim ? lim : ComputeLimitImpl(New( - Argument.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim1 ? lim1 : Argument), + WithoutTheDestination(Argument.ComputeLimitDivideEtImpera(x, dist, side)) is { IsFinite: true } lim1 ? lim1 : Argument), x, dist, side); } @@ -265,7 +280,7 @@ partial record Tanf internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) => ComputeLimitImpl(this, x, dist, side) is { } lim ? lim : ComputeLimitImpl(New( - Argument.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim1 ? lim1 : Argument), + WithoutTheDestination(Argument.ComputeLimitDivideEtImpera(x, dist, side)) is { IsFinite: true } lim1 ? lim1 : Argument), x, dist, side); } @@ -274,7 +289,7 @@ partial record Cotanf internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) => ComputeLimitImpl(this, x, dist, side) is { } lim ? lim : ComputeLimitImpl(New( - Argument.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim1 ? lim1 : Argument), + WithoutTheDestination(Argument.ComputeLimitDivideEtImpera(x, dist, side)) is { IsFinite: true } lim1 ? lim1 : Argument), x, dist, side); } diff --git a/Sources/Tests/UnitTests/Calculus/ASubstitutedValueIsFoldedTest.cs b/Sources/Tests/UnitTests/Calculus/ASubstitutedValueIsFoldedTest.cs new file mode 100644 index 000000000..412015100 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ASubstitutedValueIsFoldedTest.cs @@ -0,0 +1,47 @@ +// +// 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 Xunit; +using static AngouriMath.Entity; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A one-sided limit at a finite point is taken by moving the variable out to infinity, and the value read off there kept + /// the infinity in it: x^2 + x - 1 from the left at 0 came back to the descent as -1 - 1/+oo + 1/+oo^2. + /// The sine above it asked whether that was finite, was told no, and declined -- so sin(x^2 + x - 1) from + /// the left at 0 was left unevaluated, as on 2.6.0. Found by limitcheck at DEPTH=3, SEED=26; no issue + /// was filed. + /// + [Trait("Area", "Calculus")] + public sealed class ASubstitutedValueIsFoldedTest + { + [Theory] + [InlineData("sin(x^2 + x - 1)", ApproachFrom.Left, "sin(-1)")] + [InlineData("cos(x^2 + x + 2)", ApproachFrom.Left, "cos(2)")] + [InlineData("sin(x^2 + x - 1)", ApproachFrom.BothSides, "sin(-1)")] + [InlineData("sin(x^2 + x - 1)", ApproachFrom.Right, "sin(-1)")] + public void IsTheValueAtTheDestination(string input, ApproachFrom side, string expected) + => Assert.Equal(MathS.FromString(expected), MathS.FromString(input).Limit("x", 0, side)); + + // Answered as a quotient of the two, which is the tangent's value. + [Theory] + [InlineData("tan(x^2 + x + 2)", ApproachFrom.Left)] + [InlineData("tan(x^2 - x - 1)", ApproachFrom.Left)] + [InlineData("tan(x^2 + x - 1)", ApproachFrom.BothSides)] + public void IsTheTangentsValue(string input, ApproachFrom side) + { + var limit = MathS.FromString(input).Limit("x", 0, side); + var expected = MathS.FromString(input).Substitute("x", 0); + Assert.False(limit is Limitf, $"lim {side} x->0 {input} was left unevaluated"); + var difference = (limit - expected).EvalNumerical(); + Assert.True(difference is Number.Complex value && Number.Complex.Abs(value) is Number.Real abs && abs < 1e-10, + $"lim {side} x->0 {input} = {limit}, which is not {expected}"); + } + } +} diff --git a/Sources/Tests/UnitTests/Calculus/AnUndecidedLimitIsNotInfiniteTest.cs b/Sources/Tests/UnitTests/Calculus/AnUndecidedLimitIsNotInfiniteTest.cs index bda5b38ba..2fa7f4908 100644 --- a/Sources/Tests/UnitTests/Calculus/AnUndecidedLimitIsNotInfiniteTest.cs +++ b/Sources/Tests/UnitTests/Calculus/AnUndecidedLimitIsNotInfiniteTest.cs @@ -27,6 +27,8 @@ public sealed class AnUndecidedLimitIsNotInfiniteTest [InlineData("tan(x^2 + x - 1)", ApproachFrom.Left)] [InlineData("sin(x^2 + x - 1) / cos(x^2 + x - 1)", ApproachFrom.Left)] [InlineData("tan(x^2 + x - 1)", ApproachFrom.Right)] + [InlineData("tan(x^2 - x - 1)", ApproachFrom.Left)] + [InlineData("tan(x^2 + x - 1)", ApproachFrom.BothSides)] public void IsTheTangent(string input, ApproachFrom side) { var limit = MathS.FromString(input).Limit("x", 0, side); @@ -34,18 +36,6 @@ public void IsTheTangent(string input, ApproachFrom side) AssertIsTheTangent(input, side, limit); } - // These come back unevaluated: the parts' own limits from the left are not taken, and that is a decline, not a - // wrong answer. What they must not be is the quotient of the derivatives, as the first was before. - [Theory] - [InlineData("tan(x^2 - x - 1)", ApproachFrom.Left)] - [InlineData("tan(x^2 + x - 1)", ApproachFrom.BothSides)] - public void IsTheTangentOrUnevaluated(string input, ApproachFrom side) - { - var limit = MathS.FromString(input).Limit("x", 0, side); - if (limit is not Limitf) - AssertIsTheTangent(input, side, limit); - } - // An infinity has one side, and asking for it one-sidedly was left unevaluated where the limit is a value. The // Stirling limits relied on reading the first of these, unevaluated, as infinite: it is -oo, by luck. The last // two are a power whose base tends to a number and whose exponent tends to 0.