diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs index f7b02bf09..55ec3197d 100644 --- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs +++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs @@ -48,6 +48,22 @@ expr is Sumf(var any1, Mulf(Real { IsNegative: true } const1, var any2)) Mulf(Powf(var any1, var any3), Powf(var any2, var any3a)) when any3 == any3a => new Powf(any1 * any2, any3), Divf(Powf(var any1, var any3), Powf(var any2, var any3a)) when any3 == any3a => new Powf(any1 / any2, any3), + // {1} ^ n / {2} ^ (c * n) = ({1} / {2} ^ c) ^ n, and the same the other way up. + // + // The rule above pairs two powers by their exponents, and the ({}^{})^{} rule + // further up rewrites (b^c)^n as b^(c*n) -- on the child, on the way up, so it + // has already happened by the time the pair is looked at. Where it applies to + // only one of the two, which is whenever only one base is itself a power, the + // exponents stop matching and the pair is lost: (a^2)^x / (b^2)^x gathers, + // because both sides moved together, while (a^2)^x / b^x does not. + // These read that pair back. Restricted to a whole c so that nothing gains a + // root it did not have -- b^c goes into the base, rather than the exponent + // being divided. https://github.com/asc-community/AngouriMath/issues/740 + Divf(Powf(var any1, var any3), Powf(var any2, Mulf(Integer { IsPositive: true } const1, var any3a))) + when any3 == any3a => new Powf(any1 / new Powf(any2, const1), any3), + Divf(Powf(var any1, Mulf(Integer { IsPositive: true } const1, var any3)), Powf(var any2, var any3a)) + when any3 == any3a => new Powf(new Powf(any1, const1) / any2, any3), + // x / x^n Divf(var any1, Powf(var any1a, var any2)) when any1 == any1a => new Powf(any1, 1 - any2), diff --git a/Sources/Tests/UnitTests/Common/PowerQuotientGatheringTest.cs b/Sources/Tests/UnitTests/Common/PowerQuotientGatheringTest.cs new file mode 100644 index 000000000..b4c2a84ca --- /dev/null +++ b/Sources/Tests/UnitTests/Common/PowerQuotientGatheringTest.cs @@ -0,0 +1,140 @@ +// +// 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.Threading.Tasks; +using AngouriMath; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// a^p / b^p is gathered into (a/b)^p, but stopped being gathered as soon + /// as one of the bases was itself a power: (a^2)^x / (b^2)^x gathers and + /// (a^2)^x / b^x did not. The cause is that (b^c)^p is rewritten to + /// 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 + /// + public sealed class PowerQuotientGatheringTest + { + private static Entity Simplified(string expr) => expr.ToEntity().Simplify(); + + /// + /// The rewrite has to preserve the value, not just the shape. Checked at a point + /// where every base is positive, which is where a^p / b^(c*p) = (a/b^c)^p + /// holds without a branch argument. + /// + private static void AssertSameValueAt(string expr, params (string Variable, string Value)[] at) + { + var original = expr.ToEntity(); + var simplified = original.Simplify(); + Entity Substituted(Entity e) + { + foreach (var (variable, value) in at) + e = e.Substitute(variable, value.ToEntity()); + return e; + } + Assert.Equal( + Substituted(original).EvalNumerical().RealPart.EDecimal.ToDouble(), + Substituted(simplified).EvalNumerical().RealPart.EDecimal.ToDouble(), + 9); + } + + // A single power is what the gathering is for, and what the limit machinery reads a + // 1^oo off. Asserted as "one power" rather than as a string, since which of the + // equivalent single powers the complexity metric picks is not the point. + private static void AssertGathersIntoOnePower(string expr) + { + var simplified = Simplified(expr); + Assert.True(simplified is Entity.Powf, + $"{expr} came back as {simplified.Stringize()}, which is not a single power"); + } + + [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); + + [Theory] + [InlineData("(a ^ 2 + 1) ^ x / (a ^ 2) ^ x")] + [InlineData("(a ^ 2) ^ x / b ^ x")] + [InlineData("x ^ (2 * a) / y ^ a")] + [InlineData("(x ^ 3 + 1) ^ x / (x ^ 3) ^ x")] + public void GatheringDoesNotChangeTheValue(string expr) => + AssertSameValueAt(expr, ("a", "17/10"), ("b", "23/10"), ("x", "13/10"), ("y", "31/10")); + + /// + /// Why it is worth gathering. The limit machinery reads a 1^oo off a single power + /// and cannot see one in a quotient, so the same function was answered or not + /// according only to how it had been written -- and spent five and a half seconds + /// not answering. + /// + [Theory] + [InlineData("(x ^ 2 + 1) ^ x / (x ^ 2) ^ x")] + [InlineData("(x ^ 3 + 1) ^ x / (x ^ 3) ^ x")] + [InlineData("((x ^ 2 + 1) / x ^ 2) ^ x")] + public void TheQuotientFormIsAnsweredAsTheSinglePowerFormIs(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.Equal(Entity.Number.Integer.Create(1), limit.Result); + } + + // What must not change. A numeric exponent is not written as a product, so nothing + // here matches it, and x^4 / y^2 keeps the form it had. + [Theory] + [InlineData("x ^ 4 / y ^ 2")] + [InlineData("x ^ 4 / y ^ 3")] + [InlineData("a ^ 2 / b")] + 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. + [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); + + /// + /// The limits #739 fixed go through the same gathering, so they are pinned here as + /// well: a change to which quotients gather is a change to which of these are + /// answered. + /// + [Theory] + [InlineData("(x - 5) ^ x / x ^ x", "+oo", "1 / e ^ 5")] + [InlineData("(x + 1) ^ x / x ^ x", "+oo", "e")] + [InlineData("(x - 5) ^ x / x ^ x", "-oo", "1 / e ^ 5")] + public void TheSecondRemarkableLimitStillReadsWhatGatheringGivesIt( + string expr, string approach, string expected) + { + var limit = expr.ToEntity().Limit("x", approach.ToEntity()).Simplify(); + Assert.Equal(Entity.Number.Integer.Create(0), + (limit - expected.ToEntity()).Simplify()); + } + + /// + /// What is deliberately left. A fractional c would have to divide the exponent + /// rather than move into the base, so (sqrt(x) + 1)^x / sqrt(x)^x would + /// become ((sqrt(x) + 1)^2 / x)^(x/2) -- a squared numerator to buy a + /// gathered form. That is a judgement about output rather than the gap this fixes, + /// so it is pinned as it stands rather than forced. + /// + [Fact] + public void AFractionalExponentRatioIsStillNotGathered() => + Assert.False(Simplified("(sqrt(x) + 1) ^ x / sqrt(x) ^ x") is Entity.Powf); + } +}