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());