diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs index e09cc4462..922b4a769 100644 --- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs +++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs @@ -76,6 +76,25 @@ internal static partial class Patterns Sumf(Powf(Cosf(var any1), Integer(2)), Powf(Sinf(var any1a), Integer(2))) when any1 == any1a => 1, + // The same identity solved for one square rather than for 1. Only this direction: + // the two sides are interchangeable, and rewriting cos(:)^2 back as 1 - sin(:)^2 + // would undo this as fast as it fired. + Minusf(Integer(1), Powf(Sinf(var any1), Integer(2))) => new Powf(new Cosf(any1), 2), + Minusf(Integer(1), Powf(Cosf(var any1), Integer(2))) => new Powf(new Sinf(any1), 2), + + // The identity divided through by sin(:)^2 and by cos(:)^2. Knowing the one above + // and not these made the answer depend on which of the three ways an expression + // happened to be written -- https://github.com/asc-community/AngouriMath/issues/725. + Sumf(Integer(1), Powf(Tanf(var any1), Integer(2))) => new Powf(new Secantf(any1), 2), + Sumf(Powf(Tanf(var any1), Integer(2)), Integer(1)) => new Powf(new Secantf(any1), 2), + Sumf(Integer(1), Powf(Cotanf(var any1), Integer(2))) => new Powf(new Cosecantf(any1), 2), + Sumf(Powf(Cotanf(var any1), Integer(2)), Integer(1)) => new Powf(new Cosecantf(any1), 2), + + Minusf(Powf(Secantf(var any1), Integer(2)), + Powf(Tanf(var any1a), Integer(2))) when any1 == any1a => 1, + Minusf(Powf(Cosecantf(var any1), Integer(2)), + Powf(Cotanf(var any1a), Integer(2))) when any1 == any1a => 1, + Minusf(Powf(Sinf(var any1), Integer(2)), Powf(Cosf(var any1a), Integer(2))) when any1 == any1a => -1 * (new Powf(new Cosf(any1), 2) - new Powf(new Sinf(any1), 2)), Minusf(Powf(Cosf(var any1), Integer(2)), Powf(Sinf(var any1a), Integer(2))) when any1 == any1a => diff --git a/Sources/Tests/UnitTests/Common/PythagoreanIdentityTest.cs b/Sources/Tests/UnitTests/Common/PythagoreanIdentityTest.cs new file mode 100644 index 000000000..6ec52d4c9 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/PythagoreanIdentityTest.cs @@ -0,0 +1,120 @@ +// +// 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 AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// The Pythagorean identity was one syntactic pattern -- an adjacent sum of the two squares, + /// in either order -- so every other arrangement of the same fact was missed, including the + /// two forms got by dividing it through by sin^2 and by cos^2. + /// https://github.com/asc-community/AngouriMath/issues/725 + /// + public sealed class PythagoreanIdentityTest + { + /// + /// The value is what is asserted, not the condition attached to it. A tangent and a + /// secant are undefined where the cosine vanishes, so `sec(t)^2 - tan(t)^2` answering + /// `1 provided not cos(t) = 0` is a better answer than a bare 1 rather than a weaker + /// one -- the bare 1 would claim a value at a point the expression does not reach. + /// + private static void AssertSimplifies(string input, string expected) => + Assert.Equal(WithoutCondition(expected.ToEntity().Simplify()), + WithoutCondition(input.ToEntity().Simplify())); + + private static Entity WithoutCondition(Entity expr) + { + while (expr is Entity.Providedf(var inner, _)) expr = inner; + return expr; + } + + /// The arrangement that already worked, which must keep working. + [Theory] + [InlineData("sin(t) ^ 2 + cos(t) ^ 2", "1")] + [InlineData("cos(t) ^ 2 + sin(t) ^ 2", "1")] + [InlineData("a + sin(t) ^ 2 + cos(t) ^ 2", "1 + a")] + [InlineData("3 * sin(t) ^ 2 + 3 * cos(t) ^ 2", "3")] + public void TheSumOfTheSquaresIsUnaffected(string input, string expected) => + AssertSimplifies(input, expected); + + /// The same identity solved for one square rather than for 1. + [Theory] + [InlineData("1 - sin(t) ^ 2", "cos(t) ^ 2")] + [InlineData("1 - cos(t) ^ 2", "sin(t) ^ 2")] + [InlineData("1 - sin(t) ^ 2 + a", "cos(t) ^ 2 + a")] + [InlineData("1 - sin(t) ^ 2 - cos(t) ^ 2", "0")] + [InlineData("1 - cos(t) ^ 2 - sin(t) ^ 2", "0")] + public void SolvedForOneSquare(string input, string expected) => + AssertSimplifies(input, expected); + + /// + /// The identity divided through by sin^2 and by cos^2. Neither was known in any form: + /// not the sum, not the difference, and not with the reciprocal written out as a + /// quotient rather than as a cosecant or a secant. + /// + [Theory] + [InlineData("1 + tan(t) ^ 2", "sec(t) ^ 2")] + [InlineData("1 + cotan(t) ^ 2", "csc(t) ^ 2")] + [InlineData("sec(t) ^ 2 - tan(t) ^ 2", "1")] + [InlineData("csc(t) ^ 2 - cotan(t) ^ 2", "1")] + public void DividedThroughBySquareOfSineOrCosine(string input, string expected) => + AssertSimplifies(input, expected); + + /// + /// The reporter's first expression in + /// https://github.com/asc-community/AngouriMath/issues/557. The second is not here; see + /// for what it still wants. + /// + [Fact] + public void TheFirstComplexTrigonometricStatementOf557() => + AssertSimplifies("(sin(2 * t) * csc(t)) ^ 2 / 4 - cos(2 * t) - sin(t) ^ 2", "0"); + + /// + /// What the rules above do not reach, recorded rather than claimed. Every rule here + /// matches a pattern, so the two parts of the identity have to be *adjacent* in the + /// tree for one to fire. Written on its own each of these reduces -- + /// 1 + cotan(t)^2 is csc(t)^2 and csc(t)^2 - 1/sin(t)^2 is 0 -- + /// but in a sum of three terms the sorting separates the pair before the rules are + /// tried, and nothing puts it back. + /// + /// Reaching these wants the identity stated over the *gathered* terms of a sum rather + /// than over an adjacent pair, which is a larger change than this one and is left for + /// its own. The same gap is what still stands between + /// https://github.com/asc-community/AngouriMath/issues/557's second expression and 0. + /// Written in sines and cosines throughout, all four already reduce, so nothing here + /// is a wrong answer -- only an unreduced one. + /// + [Theory(Skip = "Wants the identity over the gathered terms of a sum, not an adjacent pair")] + [InlineData("1 + tan(t) ^ 2 - 1 / cos(t) ^ 2", "0")] + [InlineData("1 + cotan(t) ^ 2 - 1 / sin(t) ^ 2", "0")] + [InlineData("1 / sin(t) ^ 2 - (1 + cotan(t) ^ 2)", "0")] + [InlineData("(cos(2 * t) * sin(t) ^ 6 * (-1) + cos(t) * sin(t) ^ 5 * sin(2 * t)" + + " - sin(2 * t) ^ 2 * sin(t) ^ 4 / 4) / sin(t) ^ 8 - 1" + + " + (sin(2 * t) * csc(t)) ^ 2 / 4 - cos(2 * t) - sin(t) ^ 2", "0")] + public void TheThirdTermOfTheIdentityHasToBeAdjacent(string input, string expected) => + AssertSimplifies(input, expected); + + /// + /// The same statements written in sines and cosines, which do reduce. That is what makes + /// the gap above a matter of spelling rather than of the mathematics being out of reach. + /// + /// The cosine half of the first pair is not among them: `1 + sin(t)^2/cos(t)^2 - + /// 1/cos(t)^2` collapses its quotient back to a tangent and stops at + /// `1 - 1/cos(t)^2 + tan(t)^2`, where the sine half reduces. That asymmetry is the same + /// adjacency gap seen from the other side and is recorded, not fixed, here. + /// + [Theory] + [InlineData("1 + cos(t) ^ 2 / sin(t) ^ 2 - 1 / sin(t) ^ 2", "0")] + [InlineData("1 / sin(t) ^ 2 - (1 + cos(t) ^ 2 / sin(t) ^ 2)", "0")] + [InlineData("(1 - sin(t) ^ 2 - cos(t) ^ 2) / sin(t) ^ 2", "0")] + [InlineData("(1 - sin(t) ^ 2 - cos(t) ^ 2) / cos(t) ^ 2", "0")] + public void TheSameStatementsInSinesAndCosines(string input, string expected) => + AssertSimplifies(input, expected); + } +} diff --git a/Sources/Tests/UnitTests/PatternsTest/MultipleAngleTest.cs b/Sources/Tests/UnitTests/PatternsTest/MultipleAngleTest.cs index f31b0feff..96aabdfa7 100644 --- a/Sources/Tests/UnitTests/PatternsTest/MultipleAngleTest.cs +++ b/Sources/Tests/UnitTests/PatternsTest/MultipleAngleTest.cs @@ -24,20 +24,18 @@ public sealed class MultipleAngleTest [InlineData("cos(2 * x) - (1 - 2 * sin(x) ^ 2)")] [InlineData("cos(2 * x) - (cos(x) ^ 2 - sin(x) ^ 2)")] [InlineData("sin(4 * x) - 2 * sin(2 * x) * cos(2 * x)")] + // Was pinned separately as PythagoreanPairAcrossASubtractionIsStillMissed, which + // recorded that this form stops at 1 - cos(x)^2 - sin(x)^2 because sin^2 + cos^2 = 1 + // was matched only as an adjacent *sum* and here the pair sits either side of a + // subtraction. The identity solved for one square rather than for 1 -- + // 1 - cos(:)^2 = sin(:)^2 -- closes that arrangement, so it belongs here now: + // https://github.com/asc-community/AngouriMath/issues/725. Nothing about opening the + // angle changed, and the note that this was not something the angle expansion could + // reach was correct. + [InlineData("cos(2 * x) - (2 * cos(x) ^ 2 - 1)")] public void IdentitiesReduceToZero(string input) => Assert.Equal(Entity.Number.Integer.Create(0), input.ToEntity().Simplify()); - // The same identity written as cos(2x) - (2cos(x)^2 - 1) gets as far as - // 1 - cos(x)^2 - sin(x)^2 and stops: sin^2 + cos^2 = 1 is matched as a pair, and - // there the pair sits either side of a subtraction. That is the same - // pairwise-versus-flattened gap as - // https://github.com/asc-community/AngouriMath/issues/531, in a product of trigonometric terms - // rather than a sum, and is not something opening the angle can reach. - [Fact] - public void PythagoreanPairAcrossASubtractionIsStillMissed() => - Assert.Equal("1 - cos(x) ^ 2 - sin(x) ^ 2".ToEntity(), - "cos(2 * x) - (2 * cos(x) ^ 2 - 1)".ToEntity().Simplify()); - [Fact] public void DoubleAngleAndSquareCombine() => Assert.Equal("cos(x) ^ 2".ToEntity(), "cos(2 * x) + sin(x) ^ 2".ToEntity().Simplify());