diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs index 34b418076..a6c6867fc 100644 --- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs +++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs @@ -58,7 +58,18 @@ internal static partial class Patterns Minusf(var any1, var any1a) when any1 == any1a => 0, // a ^ b * c ^ b = (a * c) ^ b - Mulf(Powf(var any1, var any2), Powf(var any3, var any2a)) when any2 == any2a => new Powf(any1 * any3, any2), + // + // Guarded like its twin in PowerRules, which is where this identity is written + // out and explained: true for a whole b whatever the signs, and for positive + // real bases whatever the exponent, and false outside those two -- + // sqrt(x) * sqrt(y) became sqrt(x * y), which at x = y = -1 is 1 where the + // product is -1. https://github.com/asc-community/AngouriMath/issues/801 + Mulf(Powf(var any1, var any2), Powf(var any3, var any2a)) + when any2 == any2a + && (any2 is Integer + || (any1.Evaled is Real { IsPositive: true } + && any3.Evaled is Real { IsPositive: true })) + => new Powf(any1 * any3, any2), Sumf or Minusf when CollectCommonFactors(x) is { } collected => collected, diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs index 27f154766..8f17ff7d2 100644 --- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs +++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs @@ -56,7 +56,30 @@ expr is Sumf(var any1, Mulf(Real { IsNegative: true } const1, var any2)) => new Powf(any1, any2 * any3), // {1} ^ n * {2} ^ n = ({1} * {2}) ^ n - Mulf(Powf(var any1, var any3), Powf(var any2, var any3a)) when any3 == any3a => new Powf(any1 * any2, any3), + // + // The same condition the ({}^{})^{} rule above carries, and missing here for the + // same reason -- both were written unconditionally and #752 only looked at one of + // them. True for a whole n whatever the signs, since a^3 b^3 is (ab)(ab)(ab), and + // for positive real bases whatever the exponent. Outside those two the two sides + // can differ by a full turn of the argument: + // + // sqrt(x) * sqrt(y) came back as sqrt(x * y), and at x = y = -1 the first + // is i * i = -1 while the second is sqrt(1) = 1 + // + // https://github.com/asc-community/AngouriMath/issues/801 + Mulf(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), + // 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), // {1} ^ n / {2} ^ (c * n) = ({1} / {2} ^ c) ^ n, and the same the other way up. diff --git a/Sources/Tests/UnitTests/PatternsTest/PowerProductBranchTest.cs b/Sources/Tests/UnitTests/PatternsTest/PowerProductBranchTest.cs new file mode 100644 index 000000000..c3ba52834 --- /dev/null +++ b/Sources/Tests/UnitTests/PatternsTest/PowerProductBranchTest.cs @@ -0,0 +1,90 @@ +// +// 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; +using AngouriMath; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.PatternsTest +{ + /// + /// {1}^n * {2}^n = ({1} * {2})^n was applied for every exponent, and it is false + /// across the branch cuts: sqrt(-1) * sqrt(-1) is i * i = -1 while + /// sqrt((-1)(-1)) is sqrt(1) = 1. + /// https://github.com/asc-community/AngouriMath/issues/801 + /// + /// + /// The same mistake as https://github.com/asc-community/AngouriMath/issues/752, in two + /// rules that sit immediately below the one #752 fixed and were not looked at then. + /// + [Trait("Area", "PatternsTest")] + public sealed class PowerProductBranchTest + { + /// + /// The property, checked where a branch error shows: both bases negative. Compared + /// against the expression it came from rather than against an expected form, since + /// what matters is that simplifying did not change the value. + /// + [Theory] + [InlineData("x ^ (1/2) * y ^ (1/2)")] + [InlineData("sqrt(x) * sqrt(y)")] + [InlineData("x ^ (1/3) * y ^ (1/3)")] + [InlineData("x ^ (3/2) * y ^ (3/2)")] + public void SimplifyingKeepsTheValueAtNegativeBases(string expr) + { + var simplified = expr.ToEntity().Simplify(); + foreach (var (x, y) in new[] { (-1.0, -1.0), (-2.0, -3.0), (-0.5, -4.0), (-1.5, 2.5) }) + { + var before = expr.ToEntity().Substitute("x", x).Substitute("y", y).EvalNumerical(); + var after = simplified.Substitute("x", x).Substitute("y", y).EvalNumerical(); + var difference = Math.Abs(before.RealPart.EDecimal.ToDouble() - after.RealPart.EDecimal.ToDouble()) + + Math.Abs(before.ImaginaryPart.EDecimal.ToDouble() - after.ImaginaryPart.EDecimal.ToDouble()); + Assert.True(difference < 1e-9, + $"{expr} simplified to {simplified.Stringize()}, which at x = {x}, y = {y} " + + $"is {after.Stringize()} rather than {before.Stringize()}"); + } + } + + /// + /// 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. + /// https://github.com/asc-community/AngouriMath/issues/802 + /// + [Fact] + public void TheQuotientFormIsStillWrongAndThatIsRecorded() + { + 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()}"); + } + + /// + /// The rule must keep firing where it is sound, or this would be a fix that removes + /// an answer. A whole exponent is safe whatever the signs, and positive bases are + /// safe whatever the exponent. + /// + [Theory] + [InlineData("sqrt(2) * sqrt(3)", "sqrt(6)")] + [InlineData("x ^ 2 * y ^ 2", "(x * y) ^ 2")] + [InlineData("x ^ 3 * y ^ 3", "(x * y) ^ 3")] + public void TheSoundCasesStillGather(string expr, string expected) + { + var simplified = expr.ToEntity().Simplify(); + Assert.Equal(expected.ToEntity().Simplify(), simplified); + } + } +}