From c49aba8b2ea33be0782e4bb00bfa9fe0b6af5206 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 5 Aug 2026 09:05:58 +0000 Subject: [PATCH] Know the Pythagorean identity in more than one arrangement (#725) sin(x)^2 + cos(x)^2 was one syntactic pattern -- an adjacent sum of the two squares, in either order -- so the same fact written any other way was not recognised. The library's answer depended on which of several equivalent spellings an expression happened to arrive in: 1 - sin(t)^2 unchanged, where it is cos(t)^2 1 - sin(t)^2 - cos(t)^2 unchanged, where it is 0 1 + tan(t)^2 unchanged, where it is sec(t)^2 1 + cotan(t)^2 unchanged, where it is csc(t)^2 sec(t)^2 - tan(t)^2 unchanged, where it is 1 csc(t)^2 - cotan(t)^2 unchanged, where it is 1 Two of those are the identity solved for one square rather than for 1; four are the identity divided through by sin^2 and by cos^2. Eight rules, no machinery. The direction matters and is one-way on purpose. 1 - sin(:)^2 becomes cos(:)^2 and not the reverse: the two sides are interchangeable, so stating it both ways would undo each rewrite as fast as it fired. MultipleAngleTest.PythagoreanPairAcrossASubtractionIsStillMissed pinned this gap from the other side -- cos(2x) - (2cos(x)^2 - 1) stopped at 1 - cos(x)^2 - sin(x)^2 because the pair sat either side of a subtraction. It reduces to 0 now, so the case moves into IdentitiesReduceToZero with the old note kept. What that note said about the angle expansion not being able to reach it was right, and nothing about the expansion changed. What this does not reach is recorded rather than claimed, as a skipped theory with the diagnosis: every rule here matches a pattern, so the two parts have to be adjacent in the tree. In a sum of three terms the sorting separates the pair first, which is why 1 + cotan(t)^2 - 1/sin(t)^2 does not reduce although 1 + cotan(t)^2 is csc(t)^2 and csc(t)^2 - 1/sin(t)^2 is 0. Written in sines and cosines the same statements do reduce, so nothing there is a wrong answer, only an unreduced one. Reaching them wants the identity stated over the gathered terms of a sum, which is a larger change; it is also the last thing between the second expression in #557 and 0. The first expression in #557 is answered and is pinned here. 19 regression cases, one skipped. Tests 4825 passing with none failing, F# 130 of 130, corpus 112/117 with 0 wrong, 0 error and 0 timeout. Co-Authored-By: Claude Opus 5 --- .../Patterns/Patterns.Trigonometry.cs | 19 +++ .../Common/PythagoreanIdentityTest.cs | 120 ++++++++++++++++++ .../PatternsTest/MultipleAngleTest.cs | 20 ++- 3 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 Sources/Tests/UnitTests/Common/PythagoreanIdentityTest.cs 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());