diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs index 95d989477..6c350eed6 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs @@ -197,9 +197,41 @@ private static Entity ApplySecondRemarkable(Entity expr, Variable x, Entity dest EvalAssumingContinuous((xPlusOne - 1).Limit(x, dest)) == 0 && DivergesInMagnitude(xPower, x, dest) => MathS.e.Pow(xPower * (xPlusOne - 1)), + // a(x)^n / b(x)^n, the same limit written as a quotient. The simplifier used + // to gather this into (a/b)^n for us, which is how the shape reached the rule + // above at all -- and that gathering is false across the branch cuts, since + // sqrt(2)/sqrt(-3) is -0.8165i where (2/-3)^(1/2) is +0.8165i. + // https://github.com/asc-community/AngouriMath/issues/802 + // + // Read here instead, it is sound: a limit only needs the identity to hold in a + // neighbourhood of the destination, and both bases are required to be + // eventually positive on the way there -- where their arguments are both zero + // and there is no turn of the argument to lose. That is checkable, which is + // exactly what it is not in the simplifier, where the expression has no + // destination to be near. + Divf(Powf(var numeratorBase, var power), Powf(var denominatorBase, var powerAgain)) when + power == powerAgain && numeratorBase.ContainsNode(x) && denominatorBase.ContainsNode(x) + && power.ContainsNode(x) + && IsEventuallyPositive(numeratorBase, x, dest) && IsEventuallyPositive(denominatorBase, x, dest) + && EvalAssumingContinuous((numeratorBase / denominatorBase - 1).Limit(x, dest)) == 0 + && DivergesInMagnitude(power, x, dest) => + MathS.e.Pow(power * (numeratorBase / denominatorBase - 1)), + _ => expr }; + /// + /// Whether a base stays positive on the approach to , which is + /// what makes gathering a quotient of two powers into one power sound *here* when it + /// is not sound in general: two positive bases have argument zero apiece, so their + /// quotient's argument cannot leave the principal branch. + /// + private static bool IsEventuallyPositive(Entity expr, Variable x, Entity dest) + { + var limit = EvalAssumingContinuous(expr.Limit(x, dest)); + return limit == Real.PositiveInfinity || limit is Real { IsPositive: true }; + } + /// /// How many times over may be re-read into an /// expression that 's simplification diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs index 8f17ff7d2..4c22c6034 100644 --- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs +++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs @@ -73,14 +73,20 @@ expr is Sumf(var any1, Mulf(Real { IsNegative: true } const1, var any2)) || (any1.Evaled is Real { IsPositive: true } && any2.Evaled is Real { IsPositive: true })) => new Powf(any1 * any2, any3), - // The quotient form has the same hole -- sqrt(2) / sqrt(-3) is -0.8165i while - // (2 / -3)^(1/2) is +0.8165i -- and is deliberately left unguarded, because - // guarding it costs answers rather than only shapes. It is what lets the limit - // machinery read a 1^oo out of a quotient, so `(x^2 + 1)^x / (x^2)^x` stops - // being answered at all, which is the whole of #739 and #740. Which of the two - // to prefer is a maintainer's call and is filed separately rather than taken - // here. https://github.com/asc-community/AngouriMath/issues/802 - Divf(Powf(var any1, var any3), Powf(var any2, var any3a)) when any3 == any3a => new Powf(any1 / any2, any3), + // Same condition, same reason -- sqrt(2) / sqrt(-3) is -0.8165i where + // (2 / -3)^(1/2) is +0.8165i. https://github.com/asc-community/AngouriMath/issues/802 + // + // This gathering was what let the limit machinery read a 1^oo out of a quotient, + // so guarding it here used to cost `(x^2 + 1)^x / (x^2)^x` its limit. The limit + // reader now recognises the quotient itself, where the identity is checkable + // because there is a destination to be near and the bases can be required to be + // positive on the way to it -- see ApplySecondRemarkable. + Divf(Powf(var any1, var any3), Powf(var any2, var any3a)) + when any3 == any3a + && (any3 is Integer + || (any1.Evaled is Real { IsPositive: true } + && any2.Evaled is Real { IsPositive: true })) + => new Powf(any1 / any2, any3), // {1} ^ n / {2} ^ (c * n) = ({1} / {2} ^ c) ^ n, and the same the other way up. // diff --git a/Sources/Tests/UnitTests/Common/PowerQuotientGatheringTest.cs b/Sources/Tests/UnitTests/Common/PowerQuotientGatheringTest.cs index 66309f0d6..03bc4c1c2 100644 --- a/Sources/Tests/UnitTests/Common/PowerQuotientGatheringTest.cs +++ b/Sources/Tests/UnitTests/Common/PowerQuotientGatheringTest.cs @@ -1,10 +1,11 @@ -// +// // Copyright (c) 2019-2022 Angouri. // AngouriMath is licensed under MIT. // Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. // Website: https://am.angouri.org. // +using System.Linq; using System.Threading.Tasks; using AngouriMath; using AngouriMath.Extensions; @@ -19,6 +20,19 @@ namespace AngouriMath.Tests.Common /// b^(c*p) on the child, on the way up, so by the time the pair is looked at it /// has already happened -- and where it applies to only one of the two, the exponents /// no longer match. https://github.com/asc-community/AngouriMath/issues/740 + /// + /// That gathering no longer happens in Simplify, and these tests moved with + /// it. (a/b)^p is not a^p / b^p across the branch cuts -- + /// sqrt(2)/sqrt(-3) is -0.8165i where (2/-3)^(1/2) is + /// +0.8165i -- so the rule is now conditioned like the rest of its family + /// (https://github.com/asc-community/AngouriMath/issues/802). + /// + /// Nothing is lost by that, because the gathering was a *means*: #740 wanted it so the + /// limit machinery could read a 1^oo out of a quotient. The limit reader now + /// recognises the quotient itself, where the identity is checkable -- there is a + /// destination to be near, and both bases can be required to stay positive on the way to + /// it. So the assertions below moved from the mechanism to the outcome it existed for: + /// they ask whether the limit is answered, not whether Simplify prints one power. /// [Trait("Area", "Common")] public sealed class PowerQuotientGatheringTest @@ -56,15 +70,23 @@ private static void AssertGathersIntoOnePower(string expr) $"{expr} came back as {simplified.Stringize()}, which is not a single power"); } + /// + /// The quotients #740 was about, asked as the question it was really asking: does the + /// limit come out? The shapes it used to assert -- a single power in Simplify's + /// output -- are no longer produced, and should not be: see the note on the class. + /// [Theory] - [InlineData("(a ^ 2 + 1) ^ x / (a ^ 2) ^ x")] [InlineData("(x ^ 2 + 1) ^ x / (x ^ 2) ^ x")] [InlineData("(x ^ 3 + 1) ^ x / (x ^ 3) ^ x")] - [InlineData("(a ^ 2) ^ x / b ^ x")] [InlineData("(x ^ 2) ^ x / (x ^ 2 + 1) ^ x")] - [InlineData("x ^ (2 * a) / y ^ a")] - public void AQuotientOfPowersGathersWhenOneBaseIsItselfAPower(string expr) => - AssertGathersIntoOnePower(expr); + [InlineData("(sqrt(x) + 1) ^ x / sqrt(x) ^ x")] + public void AQuotientOfPowersIsStillAnsweredAsALimit(string expr) + { + var limit = Task.Run(() => expr.ToEntity().Limit("x", "+oo").Simplify()); + Assert.True(limit.Wait(System.TimeSpan.FromSeconds(30)), $"{expr} did not terminate"); + Assert.False(limit.Result.Nodes.Any(node => node is Entity.Limitf), + $"{expr} came back unevaluated: {limit.Result.Stringize()}"); + } [Theory] [InlineData("(a ^ 2 + 1) ^ x / (a ^ 2) ^ x")] @@ -101,14 +123,19 @@ public void AQuotientWithNumericExponentsIsUnaffected(string expr) => Assert.False(Simplified(expr) is Entity.Powf, $"{expr} was gathered into {Simplified(expr).Stringize()}"); - // What already worked, and still does. + /// + /// These used to be asserted to gather into one power. They no longer do, because + /// their bases are symbolic and nothing can say the quotient stays on the principal + /// branch -- which is the whole of #802. What must still hold is that simplifying + /// them does not change what they are, so that is what is asserted. + /// [Theory] [InlineData("(y + 1) ^ x / y ^ x")] [InlineData("a ^ x / b ^ x")] [InlineData("(a ^ 2) ^ x / (b ^ 2) ^ x")] [InlineData("(x + 1) ^ (2 * x) / x ^ (2 * x)")] - public void TheQuotientsThatAlreadyGatheredStillDo(string expr) => - AssertGathersIntoOnePower(expr); + public void TheQuotientsThatNoLongerGatherKeepTheirValue(string expr) => + AssertSameValueAt(expr, ("a", "17/10"), ("b", "23/10"), ("x", "13/10"), ("y", "31/10")); /// /// The limits #739 fixed go through the same gathering, so they are pinned here as @@ -141,11 +168,11 @@ public void TheSecondRemarkableLimitStillReadsWhatGatheringGivesIt( /// run ahead of it. /// [Fact] - public void AFractionalExponentRatioGathersOnceNothingFlattensItFirst() + public void AFractionalExponentRatioKeepsItsValue() { - var simplified = Simplified("(sqrt(x) + 1) ^ x / sqrt(x) ^ x"); - Assert.True(simplified is Entity.Powf, - $"came back as {simplified.Stringize()}"); + // The gathering this used to assert is gone with #802, and the limit it was + // wanted for is covered by AQuotientOfPowersIsStillAnsweredAsALimit above. What + // is checked here is that simplifying still does not change the value. AssertSameValueAt("(sqrt(x) + 1) ^ x / sqrt(x) ^ x", ("x", "13/10")); } } diff --git a/Sources/Tests/UnitTests/PatternsTest/PowerProductBranchTest.cs b/Sources/Tests/UnitTests/PatternsTest/PowerProductBranchTest.cs index c3ba52834..ad4e4ef39 100644 --- a/Sources/Tests/UnitTests/PatternsTest/PowerProductBranchTest.cs +++ b/Sources/Tests/UnitTests/PatternsTest/PowerProductBranchTest.cs @@ -33,6 +33,7 @@ public sealed class PowerProductBranchTest [Theory] [InlineData("x ^ (1/2) * y ^ (1/2)")] [InlineData("sqrt(x) * sqrt(y)")] + [InlineData("sqrt(x) / sqrt(y)")] [InlineData("x ^ (1/3) * y ^ (1/3)")] [InlineData("x ^ (3/2) * y ^ (3/2)")] public void SimplifyingKeepsTheValueAtNegativeBases(string expr) @@ -51,25 +52,24 @@ public void SimplifyingKeepsTheValueAtNegativeBases(string expr) } /// - /// The quotient form has the same hole and is deliberately still open: - /// sqrt(2) / sqrt(-3) is -0.8165i while (2 / -3)^(1/2) is - /// +0.8165i. It is left because guarding it costs *answers* rather than only - /// shapes -- it is what lets the limit machinery read a 1^oo out of a quotient, and - /// with it guarded (x^2 + 1)^x / (x^2)^x stops having a limit at all, which - /// is the whole of #739 and #740. Pinned here so that the day it is fixed, this - /// test fails and says where to look. + /// The quotient form had the same hole -- sqrt(2) / sqrt(-3) is + /// -0.8165i where (2 / -3)^(1/2) is +0.8165i -- and is fixed + /// too. It was left out of the first pass because guarding it cost *answers*: the + /// gathering was what let the limit machinery read a 1^oo out of a quotient. + /// The limit reader now recognises the quotient itself, where the identity is + /// checkable, so the guard costs nothing. /// https://github.com/asc-community/AngouriMath/issues/802 /// [Fact] - public void TheQuotientFormIsStillWrongAndThatIsRecorded() + public void TheQuotientFormKeepsItsValueToo() { var simplified = "sqrt(x) / sqrt(y)".ToEntity().Simplify(); var before = "sqrt(x) / sqrt(y)".ToEntity().Substitute("x", 2).Substitute("y", -3).EvalNumerical(); var after = simplified.Substitute("x", 2).Substitute("y", -3).EvalNumerical(); Assert.True( - Math.Abs(before.ImaginaryPart.EDecimal.ToDouble() - after.ImaginaryPart.EDecimal.ToDouble()) > 1e-9, - $"the quotient form now agrees at x = 2, y = -3 -- #802 looks fixed, so this test " - + $"should become an assertion that it stays fixed. Simplified: {simplified.Stringize()}"); + Math.Abs(before.ImaginaryPart.EDecimal.ToDouble() - after.ImaginaryPart.EDecimal.ToDouble()) < 1e-9, + $"sqrt(x) / sqrt(y) simplified to {simplified.Stringize()}, which at x = 2, y = -3 " + + $"is {after.Stringize()} rather than {before.Stringize()}"); } ///